Webパフォーマンス最適化完全ガイド2026 - Core Web Vitals、INP、Speculation Rules APIまで徹底解説
Webパフォーマンス最適化完全ガイド2026
Webパフォーマンスは、ユーザー体験、SEO、コンバージョン率に直接影響する重要な要素です。本記事では、2026年時点での最新のパフォーマンス最適化技術を徹底解説します。
目次
- Core Web Vitalsの理解と最適化
- INP(Interaction to Next Paint)完全攻略
- 画像最適化の最新技術
- レンダリング戦略の選択
- Speculation Rules API
- フォント最適化
- JavaScriptパフォーマンス
- ネットワーク最適化
- 測定とモニタリング
Core Web Vitalsの理解と最適化
Core Web Vitalsとは
Googleが定義するユーザー体験の核となる指標です(2026年版)。
3つの主要指標:
-
LCP(Largest Contentful Paint): 最大コンテンツの描画時間
- 目標: 2.5秒以内
-
INP(Interaction to Next Paint): インタラクション応答性
- 目標: 200ms以内(FIDから置き換え)
-
CLS(Cumulative Layout Shift): 累積レイアウトシフト
- 目標: 0.1以下
LCP最適化
// LCP要素の特定と最適化
class LCPOptimizer {
private observer: PerformanceObserver;
constructor() {
this.observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP要素:', lastEntry.element);
console.log('LCP時間:', lastEntry.startTime);
// LCP要素にpreloadヒントを追加
this.optimizeLCPElement(lastEntry.element as HTMLElement);
});
this.observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
private optimizeLCPElement(element: HTMLElement) {
if (element.tagName === 'IMG') {
const img = element as HTMLImageElement;
// fetchpriorityを設定
img.setAttribute('fetchpriority', 'high');
// preload linkを追加
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = img.src;
if (img.srcset) {
link.setAttribute('imagesrcset', img.srcset);
link.setAttribute('imagesizes', img.sizes || '100vw');
}
document.head.appendChild(link);
}
}
}
// 使用
new LCPOptimizer();
LCP改善の具体策
<!-- 悪い例 -->
<img src="/hero.jpg" alt="Hero" loading="lazy" />
<!-- 良い例 -->
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high" />
<img
src="/hero.jpg"
alt="Hero"
fetchpriority="high"
decoding="async"
width="1200"
height="600"
/>
// 動的インポートでJavaScriptを遅延読み込み
// 悪い例
import HeavyComponent from './HeavyComponent';
// 良い例
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// さらに良い例(viewport内に入ったら読み込み)
function LazyLoadComponent() {
const [Component, setComponent] = useState<React.ComponentType | null>(null);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
import('./HeavyComponent').then(mod => {
setComponent(() => mod.default);
});
observer.disconnect();
}
},
{ rootMargin: '50px' }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={ref}>
{Component ? <Component /> : <Skeleton />}
</div>
);
}
CLS最適化
/* 画像・動画に明示的なサイズを指定 */
img, video {
/* アスペクト比を維持しながらサイズを確保 */
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
/* フォント読み込み中のレイアウトシフト防止 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: optional; /* FOIT/FOUTを防ぐ */
size-adjust: 95%; /* フォールバックフォントとのサイズ調整 */
}
/* 広告スペースの事前確保 */
.ad-container {
min-height: 250px; /* 広告の最小高さを確保 */
background: #f0f0f0;
}
// 動的コンテンツのCLS対策
class CLSOptimizer {
// コンテンツ挿入前にスペースを確保
static async loadDynamicContent(
container: HTMLElement,
fetchFn: () => Promise<string>
) {
// プレースホルダーで高さを確保
const placeholder = document.createElement('div');
placeholder.style.minHeight = '200px';
placeholder.className = 'skeleton';
container.appendChild(placeholder);
const content = await fetchFn();
// コンテンツを挿入(高さは維持される)
container.innerHTML = content;
}
// 画像読み込み時のCLS防止
static setupImagePlaceholders() {
document.querySelectorAll('img[data-src]').forEach((img) => {
const element = img as HTMLImageElement;
// アスペクト比を計算してプレースホルダーを設定
const width = parseInt(element.getAttribute('width') || '0');
const height = parseInt(element.getAttribute('height') || '0');
if (width && height) {
element.style.aspectRatio = `${width} / ${height}`;
}
});
}
}
INP(Interaction to Next Paint)完全攻略
INPとは
INPは2024年3月にFIDを置き換えた新しい指標で、ユーザーインタラクションの応答性を測定します。
// INPの測定
class INPMonitor {
private observer: PerformanceObserver;
constructor() {
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const inp = entry as PerformanceEventTiming;
if (inp.duration > 200) {
console.warn('INP警告:', {
type: inp.name,
duration: inp.duration,
target: inp.target,
startTime: inp.startTime,
});
// 長時間かかったインタラクションを分析
this.analyzeSlowInteraction(inp);
}
}
});
this.observer.observe({
type: 'event',
buffered: true,
durationThreshold: 16 // 16ms以上のイベントを監視
});
}
private analyzeSlowInteraction(entry: PerformanceEventTiming) {
// 入力遅延
const inputDelay = entry.processingStart - entry.startTime;
// 処理時間
const processingTime = entry.processingEnd - entry.processingStart;
// プレゼンテーション遅延
const presentationDelay = entry.startTime + entry.duration - entry.processingEnd;
console.log('INP分析:', {
inputDelay,
processingTime,
presentationDelay,
breakdown: {
input: `${((inputDelay / entry.duration) * 100).toFixed(1)}%`,
processing: `${((processingTime / entry.duration) * 100).toFixed(1)}%`,
presentation: `${((presentationDelay / entry.duration) * 100).toFixed(1)}%`,
}
});
}
}
new INPMonitor();
INP改善策
1. 長時間タスクの分割
// 悪い例:メインスレッドをブロック
function processLargeDataset(data: any[]) {
const results = [];
for (const item of data) {
results.push(expensiveOperation(item));
}
return results;
}
// 良い例:タスクを分割
async function processLargeDatasetOptimized(data: any[]) {
const results = [];
const CHUNK_SIZE = 100;
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
const chunk = data.slice(i, i + CHUNK_SIZE);
// チャンクごとに処理
for (const item of chunk) {
results.push(expensiveOperation(item));
}
// メインスレッドに制御を返す
if (i + CHUNK_SIZE < data.length) {
await scheduler.yield(); // Scheduler API
}
}
return results;
}
// Scheduler APIのポリフィル
const scheduler = {
yield: () => new Promise(resolve => {
if ('scheduler' in window && 'yield' in window.scheduler) {
return (window.scheduler as any).yield().then(resolve);
}
// フォールバック
setTimeout(resolve, 0);
}),
};
2. Web Workersの活用
// worker.ts
self.addEventListener('message', (event) => {
const { type, data } = event.data;
if (type === 'PROCESS_DATA') {
const result = expensiveComputation(data);
self.postMessage({ type: 'RESULT', result });
}
});
function expensiveComputation(data: any) {
// 重い計算処理
return data.map((item: any) => {
// 複雑な計算...
return processedItem;
});
}
// main.ts
class WorkerPool {
private workers: Worker[] = [];
private queue: Array<{ data: any; resolve: Function }> = [];
constructor(size: number = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < size; i++) {
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module',
});
worker.addEventListener('message', (event) => {
this.handleWorkerMessage(i, event.data);
});
this.workers.push(worker);
}
}
async processData(data: any): Promise<any> {
return new Promise((resolve) => {
const availableWorker = this.workers.find((w, i) => !this.isWorkerBusy(i));
if (availableWorker) {
availableWorker.postMessage({ type: 'PROCESS_DATA', data });
this.queue.push({ data, resolve });
} else {
this.queue.push({ data, resolve });
}
});
}
private isWorkerBusy(index: number): boolean {
// ワーカーの状態管理ロジック
return false;
}
private handleWorkerMessage(workerIndex: number, message: any) {
if (message.type === 'RESULT') {
const task = this.queue.shift();
task?.resolve(message.result);
// 次のタスクを処理
const nextTask = this.queue.find((t) => !this.isWorkerBusy(workerIndex));
if (nextTask) {
this.workers[workerIndex].postMessage({
type: 'PROCESS_DATA',
data: nextTask.data,
});
}
}
}
}
// 使用
const pool = new WorkerPool();
const result = await pool.processData(largeDataset);
3. イベントハンドラーのデバウンス/スロットル
// デバウンス(最後の呼び出しから指定時間後に実行)
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = null;
func(...args);
};
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}
// スロットル(指定時間内に1回のみ実行)
function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean = false;
return function executedFunction(...args: Parameters<T>) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// 使用例
const handleSearch = debounce((query: string) => {
performSearch(query);
}, 300);
const handleScroll = throttle(() => {
updateScrollPosition();
}, 100);
// React Hooksバージョン
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// 使用
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
performSearch(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
画像最適化の最新技術
次世代フォーマットの活用
<!-- AVIF/WebPのフォールバック付き -->
<picture>
<source srcset="/image.avif" type="image/avif" />
<source srcset="/image.webp" type="image/webp" />
<img
src="/image.jpg"
alt="Description"
width="800"
height="600"
loading="lazy"
decoding="async"
/>
</picture>
レスポンシブイメージ
<!-- デバイス幅に応じた画像配信 -->
<img
src="/image-800.jpg"
srcset="
/image-400.jpg 400w,
/image-800.jpg 800w,
/image-1200.jpg 1200w,
/image-1600.jpg 1600w
"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
800px
"
alt="Responsive image"
loading="lazy"
/>
画像最適化の自動化
// Next.js Image Componentの最適な使い方
import Image from 'next/image';
export function OptimizedImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={1200}
height={630}
quality={85} // 85%が最適なバランス
placeholder="blur"
blurDataURL={generateBlurDataURL(src)}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={false} // LCP要素の場合はtrue
/>
);
}
// ブラーデータURLの生成
function generateBlurDataURL(src: string): string {
// 実装例(実際にはビルド時に生成)
return `data:image/svg+xml;base64,${btoa(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630">
<filter id="b" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="20" />
</filter>
<image
preserveAspectRatio="none"
filter="url(#b)"
href="${src}"
width="100%"
height="100%"
/>
</svg>
`)}`;
}
遅延読み込みの高度な実装
class AdvancedLazyLoader {
private observer: IntersectionObserver;
private imageQueue: Set<HTMLImageElement> = new Set();
constructor() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target as HTMLImageElement);
}
});
},
{
rootMargin: '50px', // 50px手前から読み込み開始
threshold: 0.01,
}
);
this.init();
}
private init() {
// data-src属性を持つ画像を検出
document.querySelectorAll('img[data-src]').forEach((img) => {
this.observer.observe(img);
});
}
private async loadImage(img: HTMLImageElement) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (!src) return;
// 画像を優先度付きで読み込み
const image = new Image();
if (srcset) {
image.srcset = srcset;
}
image.src = src;
// 読み込み完了を待つ
await image.decode();
// DOM更新(レイアウトシフト防止)
requestAnimationFrame(() => {
img.src = src;
if (srcset) {
img.srcset = srcset;
}
img.removeAttribute('data-src');
img.removeAttribute('data-srcset');
});
this.observer.unobserve(img);
}
}
new AdvancedLazyLoader();
レンダリング戦略の選択
SSR vs SSG vs ISR vs CSR
// Next.js 15の例
// 1. SSG(Static Site Generation)- 最速
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}
// 2. ISR(Incremental Static Regeneration)- 更新可能
export const revalidate = 3600; // 1時間ごとに再生成
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return <Product data={product} />;
}
// 3. SSR(Server-Side Rendering)- リアルタイムデータ
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
const userData = await getCurrentUser(); // リクエストごとに実行
return <DashboardView user={userData} />;
}
// 4. PPR(Partial Prerendering)- Next.js 15の新機能
export const experimental_ppr = true;
export default async function HybridPage() {
return (
<div>
{/* 静的部分 */}
<Header />
{/* 動的部分(Suspenseで囲む) */}
<Suspense fallback={<Skeleton />}>
<DynamicContent />
</Suspense>
{/* 静的部分 */}
<Footer />
</div>
);
}
ストリーミングSSR
// React 19 + Next.js 15のストリーミング
import { Suspense } from 'react';
async function SlowComponent() {
// 遅いデータフェッチ
const data = await fetchSlowData();
return <div>{data}</div>;
}
export default function Page() {
return (
<div>
<h1>即座に表示</h1>
{/* 遅いコンポーネントをストリーミング */}
<Suspense fallback={<LoadingSpinner />}>
<SlowComponent />
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<AnotherSlowComponent />
</Suspense>
</div>
);
}
Speculation Rules API
概要
Speculation Rules APIは、ユーザーが次にアクセスする可能性の高いページを事前読み込みする新しいAPI(2024年Chrome採用)です。
<!-- 基本的な使い方 -->
<script type="speculationrules">
{
"prerender": [
{
"source": "list",
"urls": ["/about", "/contact"]
}
],
"prefetch": [
{
"source": "list",
"urls": ["/products/*"],
"requires": ["anonymous-client-ip-when-cross-origin"]
}
]
}
</script>
動的なSpeculation Rules
class SmartPrefetcher {
private rules: any = {
prerender: [],
prefetch: [],
};
constructor() {
this.setupAnalytics();
}
// ユーザー行動に基づいて動的にprefetch
private setupAnalytics() {
// リンクにホバーしたら即座にprefetch
document.addEventListener('mouseover', (e) => {
const link = (e.target as HTMLElement).closest('a');
if (link && this.shouldPrefetch(link as HTMLAnchorElement)) {
this.addPrefetchRule((link as HTMLAnchorElement).href);
}
});
// スクロール位置に基づいてprerender
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const link = entry.target as HTMLAnchorElement;
this.addPrerenderRule(link.href);
}
});
},
{ rootMargin: '200px' }
);
// 重要なリンクを監視
document.querySelectorAll('a[data-prerender]').forEach((link) => {
observer.observe(link);
});
}
private shouldPrefetch(link: HTMLAnchorElement): boolean {
// 同一オリジンかチェック
const url = new URL(link.href);
if (url.origin !== location.origin) return false;
// すでにprefetch済みかチェック
if (this.rules.prefetch.includes(link.href)) return false;
// 接続速度をチェック
const connection = (navigator as any).connection;
if (connection && connection.effectiveType === '2g') return false;
return true;
}
private addPrefetchRule(url: string) {
if (this.rules.prefetch.includes(url)) return;
this.rules.prefetch.push(url);
this.updateSpeculationRules();
}
private addPrerenderRule(url: string) {
if (this.rules.prerender.includes(url)) return;
this.rules.prerender.push(url);
this.updateSpeculationRules();
}
private updateSpeculationRules() {
// 既存のspeculation rulesを削除
const existingScript = document.querySelector('script[type="speculationrules"]');
if (existingScript) {
existingScript.remove();
}
// 新しいrulesを追加
const script = document.createElement('script');
script.type = 'speculationrules';
script.textContent = JSON.stringify({
prefetch: [
{
source: 'list',
urls: this.rules.prefetch,
},
],
prerender: [
{
source: 'list',
urls: this.rules.prerender,
},
],
});
document.head.appendChild(script);
}
}
// 使用
new SmartPrefetcher();
Document Rules(より高度な使い方)
<script type="speculationrules">
{
"prefetch": [
{
"source": "document",
"where": {
"and": [
{ "href_matches": "/products/*" },
{ "not": { "href_matches": "/products/admin/*" } },
{ "selector_matches": ".product-link" }
]
},
"eagerness": "moderate"
}
],
"prerender": [
{
"source": "document",
"where": {
"href_matches": "/checkout"
},
"eagerness": "eager"
}
]
}
</script>
フォント最適化
フォント読み込み戦略
/* 最適なフォント読み込み */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: optional; /* FOIT/FOUTを防ぐ */
unicode-range: U+0020-007F; /* ASCII文字のみ */
}
/* 可変フォントの活用 */
@font-face {
font-family: 'InterVariable';
src: url('/fonts/Inter-Variable.woff2') format('woff2');
font-weight: 100 900; /* すべてのウェイトを1ファイルで */
font-display: swap;
}
/* サブセット化 */
@font-face {
font-family: 'NotoSansJP';
src: url('/fonts/NotoSansJP-subset.woff2') format('woff2');
unicode-range: U+3040-309F, U+30A0-30FF; /* ひらがな・カタカナのみ */
}
フォントpreload
<!-- 重要なフォントをpreload -->
<link
rel="preload"
href="/fonts/inter-variable.woff2"
as="font"
type="font/woff2"
crossorigin
/>
フォールバックフォントの最適化
/* システムフォントに近いサイズ調整 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
size-adjust: 105%; /* フォールバックとのサイズ差を調整 */
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
}
/* フォールバックスタック */
body {
font-family:
'CustomFont',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
'Noto Sans JP',
sans-serif;
}
JavaScriptパフォーマンス
コード分割
// ルートベース分割(自動)
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// コンポーネントベース分割
const HeavyChart = lazy(() => import('./components/HeavyChart'));
// ライブラリの遅延読み込み
async function loadMarkdownParser() {
const { marked } = await import('marked');
return marked;
}
Tree Shaking
// 悪い例:全体をインポート
import _ from 'lodash';
_.debounce(fn, 300);
// 良い例:必要な部分のみインポート
import debounce from 'lodash/debounce';
debounce(fn, 300);
// さらに良い例:Tree Shaking可能なライブラリを使用
import { debounce } from 'lodash-es';
バンドルサイズ分析
# Viteの場合
npm run build -- --report
# Next.jsの場合
npm install -D @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// 設定...
});
ネットワーク最適化
リソースヒント
<!-- DNS Prefetch -->
<link rel="dns-prefetch" href="https://api.example.com" />
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Prefetch(将来使う可能性のあるリソース) -->
<link rel="prefetch" href="/next-page.js" />
<!-- Preload(現在のページで必要なリソース) -->
<link rel="preload" href="/critical.css" as="style" />
<link rel="preload" href="/hero.jpg" as="image" />
HTTP/3とQUIC
# Nginxでの設定例
server {
listen 443 quic reuseport;
listen 443 ssl;
http3 on;
# ALT-SVCヘッダーでHTTP/3を通知
add_header Alt-Svc 'h3=":443"; ma=86400';
}
キャッシング戦略
// Service Workerでのキャッシング
const CACHE_NAME = 'v1';
const STATIC_ASSETS = ['/index.html', '/app.js', '/styles.css'];
self.addEventListener('install', (event: any) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
});
self.addEventListener('fetch', (event: any) => {
event.respondWith(
caches.match(event.request).then((response) => {
// キャッシュヒット
if (response) {
return response;
}
// ネットワークから取得
return fetch(event.request).then((response) => {
// 有効なレスポンスのみキャッシュ
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
測定とモニタリング
Web Vitalsの測定
import { onCLS, onFID, onLCP, onINP, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric: any) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
});
// ビーコンAPIで送信(ページ離脱時も確実に送信)
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', body);
} else {
fetch('/analytics', {
method: 'POST',
body,
keepalive: true,
});
}
}
// 各指標を監視
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
Performance Observer
class PerformanceMonitor {
constructor() {
this.observeLongTasks();
this.observeLayoutShifts();
this.observeResourceTiming();
}
private observeLongTasks() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('長時間タスク検出:', {
duration: entry.duration,
startTime: entry.startTime,
});
}
}
});
observer.observe({ type: 'longtask', buffered: true });
}
private observeLayoutShifts() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const layoutShift = entry as any;
if (!layoutShift.hadRecentInput) {
console.warn('レイアウトシフト:', {
value: layoutShift.value,
sources: layoutShift.sources,
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
}
private observeResourceTiming() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const resource = entry as PerformanceResourceTiming;
if (resource.duration > 1000) {
console.warn('遅いリソース:', {
name: resource.name,
duration: resource.duration,
transferSize: resource.transferSize,
});
}
}
});
observer.observe({ type: 'resource', buffered: true });
}
}
new PerformanceMonitor();
リアルユーザーモニタリング(RUM)
class RUMCollector {
private metrics: Map<string, number[]> = new Map();
collectMetric(name: string, value: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name)!.push(value);
// バッファがいっぱいになったら送信
if (this.metrics.get(name)!.length >= 10) {
this.flush(name);
}
}
private flush(metricName?: string) {
const metricsToSend = metricName
? { [metricName]: this.metrics.get(metricName) }
: Object.fromEntries(this.metrics);
fetch('/rum-endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metrics: metricsToSend,
userAgent: navigator.userAgent,
connection: (navigator as any).connection?.effectiveType,
timestamp: Date.now(),
}),
keepalive: true,
});
if (metricName) {
this.metrics.delete(metricName);
} else {
this.metrics.clear();
}
}
// ページ離脱時に送信
setupBeforeUnload() {
window.addEventListener('beforeunload', () => {
this.flush();
});
}
}
const rum = new RUMCollector();
rum.setupBeforeUnload();
まとめ
2026年のWebパフォーマンス最適化は、以下のポイントが重要です。
-
Core Web Vitals対応
- LCP < 2.5s
- INP < 200ms
- CLS < 0.1
-
画像最適化
- AVIF/WebP使用
- レスポンシブイメージ
- 遅延読み込み
-
JavaScript最適化
- コード分割
- Tree Shaking
- Web Workers活用
-
新技術の活用
- Speculation Rules API
- Partial Prerendering
- HTTP/3
-
継続的な測定
- Web Vitals監視
- RUMの実装
- Performance Observer活用
パフォーマンス最適化は一度やれば終わりではなく、継続的な改善が必要です。本記事の技術を活用して、高速で快適なWebサイトを実現してください。