- Published on
現代Webパフォーマンスの科学 深掘りガイド — Core Web Vitals、INP、LCP、CLS、RUM、Lighthouse、Critical Rendering Path、Speculation Rulesまで (2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR — 2024年はWebパフォーマンスの地殻変動だった。INP (Interaction to Next Paint) がFIDを置き換え、Core Web Vitals 3指標は LCP・CLS・INP へ再編。同年に Speculation Rules API (Chrome 121)、Partial Prerendering (Next.js 14)、HTTP/3 QUIC (全トラフィックの30%超)、Early Hints (103 Status) が標準化・一般化した。本稿は「なぜ自分のサイトが遅いのか」に答えるため、Critical Rendering Path の原理、Long Taskの分割、RUM vs Lab Data の違い、Lighthouseが外しやすい理由、Islands・Resumability (Qwik)、Image/Font最適化 (AVIF、
font-display: optional)、2025年のパフォーマンスツールスタック (Vercel Analytics、SpeedCurve、Perfetto) までを整理する。
なぜWebパフォーマンスが再び「熱い話題」になったのか
Webパフォーマンスは2010年代前半に YSlow (Yahoo) と PageSpeed Insights (Google) で一般化したあと、長らく「インフラチームのチェックリスト」レベルに止まっていた。ところが2020年にGoogleが Core Web Vitals を検索ランキングシグナルとして公式発表し、2024年にINPがFIDを置き換えたことで、パフォーマンスは 検索順位・広告CVR・直帰率 を直接左右するビジネス指標になった。
数字で見ると:
- Amazon: ページ読み込み100msの遅延で売上1%減 (2006年基準、2024年はさらに敏感)
- Walmart: LCPが1秒短縮でCVR 2%向上
- BBC: 遅延1秒ごとに10%離脱増
- Vodafone: LCP 31%改善で販売CVR 8%向上 (2021ケーススタディ)
2024年の Chrome UX Report (CrUX、実ユーザーデータ) によると、世界のトップ100万サイトのうちCore Web Vitals 3指標すべてを通過したのは 42% のみ。対してReact/Vue/AngularなどのSPAフレームワーク採用サイトの通過率は 28% と、静的サイト (65%) に比べて大きく低い。パフォーマンスは「フレームワーク選択のコスト」でもある。
本稿の目的は Core Web Vitals 3指標の定義と計測法、そして なぜ同じコードがLabでは速くFieldでは遅いのか、Long TaskとLayout Shiftをどう追跡・修正するか を原理から実務まで一息に整理することだ。パフォーマンスは「一つのテクニック」ではなく「レンダリングパイプライン全体の理解」から生まれる。
ブラウザレンダリングパイプライン — ピクセルに届くまでの旅
Webパフォーマンスを語るには、ブラウザがHTMLを受け取りピクセルを描くまでの Critical Rendering Path を知る必要がある。この経路で時間が漏れるあらゆる地点が「パフォーマンス問題」の原因だ。
1. Navigation — URL入力 / リンククリック
↓
2. DNS Lookup — example.com → 93.184.216.34
↓
3. TCP + TLS Handshake — 3-way + SSL (HTTP/1.1: ~300ms, HTTP/3: ~100ms)
↓
4. HTTP Request — GET /
↓
5. TTFB — Time To First Byte (サーバー応答)
↓
6. HTML Parsing — DOMツリー構築 (パース中に外部リソース要求)
↓
7. CSSOM Construction — CSSパース、CSSOMツリー
↓
8. Render Tree — DOM + CSSOM → 画面に描くノードのみ
↓
9. Layout (Reflow) — 各ノードの位置/サイズ計算
↓
10. Paint — ピクセル情報生成 (レイヤー単位)
↓
11. Composite — GPUでレイヤー合成
↓
12. 画面表示 — ユーザーが見る最初のピクセル (FCP)
各段階で起こる主要ボトルネック:
- DNS + TCP + TLS — 最初のバイトを受け取るまでのRTT (Round Trip Time) が3–4回。ここを HTTP/3 QUIC (0-RTT resumption) が攻める。
- TTFB — サーバー応答時間。SSRならDB + レンダリング、静的ファイルならCDNキャッシュヒットの可否が決め手。
- HTML Parsing Blocking —
<script>タグはデフォルトでパースを止める。async/deferで解消。 - CSSOM Blocking — CSSは レンダリングブロッキングリソース。CSSロード完了までRender Treeは組まれない。
- Layout —
offsetHeight読み取りのような 同期強制レイアウト (Forced Reflow) がパフォーマンスキラー。 - Paint / Composite —
will-change: transform、contain: layoutでGPUレイヤー分離を誘導。
React/VueなどのJSフレームワークを使うと、ここに JSダウンロード・パース・実行・Hydration が加わる。この追加段階がSPAがCore Web Vitalsで不利な根本原因だ。
Core Web Vitals 3指標 (2024–2025)
Googleは2020年に Core Web Vitals をユーザー体験の3本柱と定義した:
- ローディング (Loading) — LCP
- 相互作用性 (Interactivity) — FID → INP (2024.03置換)
- 視覚的安定性 (Visual Stability) — CLS
LCP — Largest Contentful Paint (Loading)
定義: ビューポート内で最も大きなコンテンツ要素 (画像、動画ポスター、テキストブロック) が画面に描かれる時点。
基準: <2.5s = Good、2.5–4.0s = Needs Improvement、>4.0s = Poor。
計測: LargestContentfulPaint PerformanceObserver API。
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP element:', entry.element)
console.log('LCP time:', entry.startTime)
console.log('LCP render time:', entry.renderTime)
console.log('LCP size:', entry.size)
}
}).observe({ type: 'largest-contentful-paint', buffered: true })
LCPが遅くなる4大主犯:
- 遅いTTFB — サーバー応答が1秒かかるとLCP
<2.5s通過は不可能。 - レンダリングブロッキングリソース —
<link rel="stylesheet">の遅延がLCPをそのまま遅らせる。 - リソースロード時間 — LCP画像はlazy-loadしてはいけない。
fetchpriority="high"を使う。 - クライアントサイドレンダリング — ReactはJSダウンロード後Hydrationが終わってからLCP要素がDOMに現れる。
LCP最適化チェックリスト:
- LCP画像に
fetchpriority="high"+loading="eager"+decoding="async" <link rel="preload" as="image" href="/hero.webp" imagesrcset="..." fetchpriority="high">- Above-the-foldコンテンツはインラインCSSで
- フォントは
font-display: optionalまたはswap - CDN + HTTP/3 + Brotli圧縮
- SSRまたはSSG (CSRを避ける)
CLS — Cumulative Layout Shift (視覚的安定性)
定義: ページロード中に予期せずレイアウトが動いた度合い。動いた要素の「影響比率」×「移動距離」を累積。
基準: <0.1 = Good、0.1–0.25 = Needs Improvement、>0.25 = Poor。
CLS公式: impact fraction × distance fraction
- Impact Fraction: ビューポートに対する移動要素の占有比
- Distance Fraction: 移動距離 / ビューポートサイズ
CLS主犯5つ:
- サイズ未指定の画像 —
<img>のwidth/height欠落 → 画像ロード後にレイアウトがずれる。 - サイズ未指定の広告/埋め込み — AdSense、YouTube embed、iframe。
- FOIT/FOUT — フォント読み込み前後で書体が変わりテキスト高さが変化。
- 動的コンテンツ挿入 — 上部にバナー/通知を挿入。
- Webフォントロード遅延 —
font-display: swapがCLSを起こす (皮肉)。
CLS最適化:
<!-- 画像サイズ明示 -->
<img src="/hero.jpg" width="1200" height="630" alt="..." />
<!-- CSS aspect-ratio で領域予約 -->
<style>
.embed { aspect-ratio: 16 / 9; }
</style>
<!-- Font fallbackサイズ調整 -->
<style>
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: optional; /* CLS防止、フォント未到達なら fallback 維持 */
size-adjust: 107%; /* fallback とサイズを一致させる */
}
</style>
<!-- Skeleton / Placeholder -->
<div class="skeleton" style="min-height: 400px;">Loading...</div>
INP — Interaction to Next Paint (2024.03公式化)
定義: ユーザーがクリック/タップ/キーボード入力をしたとき、次のフレームが描かれるまでの時間 — ページ全体の寿命で最悪 (最も遅い) 相互作用 が基準 (98パーセンタイル近傍)。
基準: <200ms = Good、200–500ms = Needs Improvement、>500ms = Poor。
INPがなぜFIDを置き換えたか:
- FID (First Input Delay) は最初の入力の「開始遅延」しか測らない (入力→ハンドラ開始)。
- しかし実際のUX問題は 入力後→画面反映まで の全時間。長いJS、再レンダリング、Layout、Paintをすべて含める必要がある。
- FIDはほとんどのサイトが
<100msで通過 → 弁別力がない。INPははるかに厳しい。
INP公式 (簡略):
INP = max(interactions) where interaction_time =
(input delay) + (processing time) + (presentation delay)
INP計測:
import { onINP } from 'web-vitals'
onINP((metric) => {
console.log('INP:', metric.value, 'ms')
console.log('Attribution:', metric.attribution)
// attribution: { interactionType, eventTarget, loafTime, ... }
})
INPが悪化する7大主犯:
- Long Task — 50ms超えのメインスレッドブロッキングJS。
- 大きなReactコンポーネントの再レンダリング — state更新→サブツリー全体再レンダリング。
- 同期ネットワーク要求 — event handler内でfetchをawait。
- 大規模DOM — 数千ノードのLayout再計算。
- 重いCSSセレクタ —
:has()、複雑なnth-child。 - 同期third-partyスクリプト — 広告、分析ツール。
- ResizeObserver/MutationObserver暴走 — コールバックが同期Layoutを引き起こす。
INP最適化 — Long Taskの分割:
// 悪い — 1000アイテムを一度に処理 (800ms Long Task)
function processItems(items) {
items.forEach(item => expensiveWork(item))
}
// 良い — scheduler.yield() (2024標準化)
async function processItems(items) {
for (const item of items) {
expensiveWork(item)
await scheduler.yield() // メインスレッドへ譲る
}
}
// 代替 — setTimeoutで譲る (旧ブラウザ)
function processItemsYield(items, i = 0) {
const deadline = performance.now() + 10
while (i < items.length && performance.now() < deadline) {
expensiveWork(items[i++])
}
if (i < items.length) {
setTimeout(() => processItemsYield(items, i), 0)
}
}
React INP特化 — useTransition:
import { useTransition, useState } from 'react'
function SearchBox() {
const [isPending, startTransition] = useTransition()
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
function handleChange(e) {
setQuery(e.target.value) // 緊急更新 (input value)
startTransition(() => {
setResults(expensiveSearch(e.target.value)) // 低優先度
})
}
// ...
}
RUM vs Lab Data — Lighthouseがよく外す理由
Webパフォーマンスデータには大きく2種類ある:
Lab Data (合成モニタリング、Synthetic)
- Lighthouse、WebPageTest、PageSpeed Insights (Labタブ)
- 統制された環境で1回計測
- 利点: 再現可能、リグレッション検知が容易、CI/CD統合可能
- 欠点: 実ユーザーのネットワーク/デバイス/相互作用と異なる
Field Data / RUM (Real User Monitoring)
- Chrome UX Report (CrUX)、Vercel Analytics、Sentry Performance、New Relic Browser、SpeedCurve
- 実ユーザーブラウザからPerformance APIで収集
- 利点: 実UXを反映、デバイス/ネットワーク分布を反映
- 欠点: ノイズが多い、デバッグ困難 (どの相互作用がINP 80msかを追跡する必要)
Lighthouseスコアと実スコアが異なる理由:
- Lighthouseは Moto G Power + 4Gシミュレーション 固定。実ユーザーはiPhone 15 + 5Gかもしれない。
- Lighthouseはページロードのみ計測 → INPはセッション全体の相互作用ベース → Labでは計測不可。
- Lighthouseは 1回計測 → 実際は分布。GoogleはCrUXの 75パーセンタイル を基準にする。
- Lighthouseには Cookie/ログインなし → 実際は認証済みユーザー画面が異なる。
- Lighthouseは viewport固定 360×640 → 実デバイス幅分布と異なる。
推奨戦略:
- Lab Data (Lighthouse): PRごとのリグレッションテスト (Lighthouse CI)、上限設定
- RUM: プロダクションモニタリング、p75 / p95指標の追跡、国別/デバイス別ドリルダウン
- 両者が一致しないとき RUMを信じよ
2025年のRUMスタック
| プロバイダ | 特徴 | 価格 (参考) |
|---|---|---|
| Vercel Speed Insights | Next.js統合、Core Web Vitals + Custom Events | 10,000 events無料 |
| Google CrUX | 月次公開データ、BigQuery | 無料 |
| Sentry Performance | エラー + パフォーマンス統合 | $26/moから |
| SpeedCurve | 競合比較、カスタムダッシュボード | $149/moから |
| New Relic Browser | APM統合 | Free tier |
| Cloudflare Web Analytics | サーバーレス、プライバシー優先 | 無料 |
| Pingdom RUM | 地理分布に強み | $14.95/moから |
リソース優先度とロード戦略
ブラウザは100個のリソースを同時に受けるわけではない。Fetch Priority、Preload Scanner、HTTP/2 Priority、HTTP/3 Priority が複雑に絡んで決める。
Resource Hints — ブラウザに先に知らせる
<!-- DNSを事前解決 -->
<link rel="dns-prefetch" href="https://api.example.com" />
<!-- 接続 (DNS + TCP + TLS) を先に開く -->
<link rel="preconnect" href="https://api.example.com" crossorigin />
<!-- リソース事前ダウンロード (現在ページ用) -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high" />
<link rel="preload" href="/main.js" as="script" />
<link rel="preload" href="/inter.woff2" as="font" type="font/woff2" crossorigin />
<!-- 次ページの事前取得 (low priority) -->
<link rel="prefetch" href="/next-page.html" />
<!-- ページ全体の事前レンダリング (Speculation Rulesに置換) -->
<link rel="prerender" href="/next-page.html" /> <!-- Deprecated -->
Fetch Priority API (Chrome 101+、Safari 17+、Firefox 132+)
<!-- LCP画像 -->
<img src="/hero.webp" fetchpriority="high" />
<!-- スクロール下領域の画像 -->
<img src="/below-fold.jpg" fetchpriority="low" loading="lazy" />
<!-- fetch() API -->
<script>
fetch('/critical.json', { priority: 'high' })
fetch('/analytics.json', { priority: 'low' })
</script>
Speculation Rules API — 2024標準化
既存の <link rel="prefetch"> と prerender の限界を越え、CSSセレクタベース でユーザーが 訪問する可能性が高いリンク を事前prerender。
<script type="speculationrules">
{
"prerender": [{
"urls": ["/product/1", "/product/2"],
"eagerness": "moderate"
}],
"prefetch": [{
"where": { "href_matches": "/product/*" },
"eagerness": "conservative"
}]
}
</script>
eagerness レベル:
immediate— 即座 (aggressive)eager— ヒント発見即時moderate— リンクにhover/touchなどconservative— リンククリック直前
Chrome 121+、実質的にLCP 0ms が可能 (prerenderされたページへの遷移時に即時表示)。
Early Hints (HTTP 103)
サーバーが最終応答 (200) の前に 103 Early Hints ステータスコードで Link: </main.css>; rel=preload ヒントを先に送る技術。
HTTP/1.1 103 Early Hints
Link: </main.css>; rel=preload; as=style
Link: </hero.webp>; rel=preload; as=image
HTTP/1.1 200 OK
Content-Type: text/html
...
Cloudflare、Fastly、Next.js (14.1+) がサポート。TTFBを待たずに主要リソースを先に取得 → LCP 200–400ms短縮可能。
画像最適化 — 全Webサイト帯域の50%
Chrome UX Reportによると平均Webページで 画像は全バイトの48%。画像最適化だけでLCPを1秒以上短縮できる。
フォーマット選定
| フォーマット | サポート | 特徴 | 圧縮率 |
|---|---|---|---|
| JPEG | 100% | 写真、非可逆 | 基準 |
| PNG | 100% | 透明度、可逆 | サイズ大 |
| WebP | 97% (IE除く) | Google、25–35%小 | JPEG比25–35%小 |
| AVIF | 93% | AV1コーデックベース、50%小 | JPEG比50%小 |
| JPEG XL | Safariのみ (実験的) | 将来候補 | AVIFと同等 |
2025年推奨: <picture> でAVIF → WebP → JPEG fallback。
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" width="1200" height="630" alt="..." loading="lazy" decoding="async" />
</picture>
Responsive Images — srcset + sizes
<img
src="/hero-800.jpg"
srcset="/hero-400.jpg 400w,
/hero-800.jpg 800w,
/hero-1600.jpg 1600w,
/hero-2400.jpg 2400w"
sizes="(max-width: 768px) 100vw,
(max-width: 1200px) 50vw,
33vw"
width="800" height="600"
alt="Hero"
/>
現代CDN — 自動フォーマット変換
- Cloudinary — URLベース変換 (
w_800,f_auto,q_auto) - imgix — 動的パラメータ
- Cloudflare Images — $5/月 100k画像
- Next.js Image —
<Image />コンポーネント (AVIF/WebP自動) - Vercel Image Optimization — ビルドタイム + オンデマンド
Lazy Loading
<!-- ネイティブlazy loading (Chrome 77+) -->
<img src="/hero.jpg" loading="lazy" />
<!-- IntersectionObserverベースのカスタム -->
<script>
const images = document.querySelectorAll('img[data-src]')
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
})
}, { rootMargin: '200px' }) // 200px手前で事前ロード
images.forEach(img => observer.observe(img))
</script>
注意: LCP画像は 絶対にlazy loadingしない。loading="eager" + fetchpriority="high"。
フォント最適化 — FOIT/FOUTとCLSの主犯
Webフォントはダウンロードまでテキストが見えない (FOIT) か、fallbackで見えていて突然変わる (FOUT) — いずれもUXを損なう。
font-display 戦略
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: swap; /* FOUT: fallback表示後に交換 — CLS発生 */
font-display: optional; /* 100ms以内に届かなければ fallback維持 — CLS 0 */
font-display: block; /* 3秒まで待機 — FOIT */
font-display: fallback; /* 100ms + 3秒 */
}
推奨: LCPテキストは font-display: optional + size-adjust でfallbackのサイズを合わせる。
Font Fallbackサイズ合わせ (size-adjust)
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: optional;
}
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
size-adjust: 107.4%; /* ArialをInterのサイズに調整 */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter-fallback', sans-serif;
}
ツール: Font Style Matcher、Fontaine (Vite plugin)。
Preload + Subset
<!-- 必須フォントpreload -->
<link rel="preload" href="/inter-latin.woff2" as="font" type="font/woff2" crossorigin />
Subset: 日本語/韓国語フォント (Noto Sans JP、Pretendard) は全グリフで3–10MB。unicode-range で必要領域のみロード。
@font-face {
font-family: 'Pretendard';
src: url('Pretendard-KR.woff2') format('woff2');
unicode-range: U+AC00-D7A3, U+1100-11FF, U+3130-318F; /* ハングル領域のみ */
}
JavaScriptロード戦略
JSはWebパフォーマンス最大の敵であり友だ。現代SPAは平均 400KB gzipped JS をロード — モバイルでパース/コンパイルだけで >800ms。
async vs defer vs module
<!-- Blocking (絶対使わない) -->
<script src="/main.js"></script>
<!-- Async: ダウンロード完了即実行、順序保証なし -->
<script src="/analytics.js" async></script>
<!-- Defer: ダウンロード並列、HTMLパース完了後に実行、順序保証 -->
<script src="/main.js" defer></script>
<!-- Module: デフォルトでdefer、順序保証 -->
<script src="/app.js" type="module"></script>
Code Splitting
Webpack/Rollup/esbuildすべてサポート。ルート/コンポーネント単位でJSを分割し初期ロード量を削減。
// React.lazy
const Heavy = React.lazy(() => import('./HeavyComponent'))
function App() {
return (
<Suspense fallback={<Skeleton />}>
<Heavy />
</Suspense>
)
}
// Next.js dynamic
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
Tree Shaking
ESMのimport構文を解析して未使用コードを除去。side-effect free 宣言が必須。
// package.json
{
"sideEffects": false,
"exports": {
".": "./dist/index.js"
}
}
Third-Party Scripts — 最大の敵
Google Analytics、Facebook Pixel、Intercomなど3rd partyスクリプト1つでINP 500msを生む。
Partytown (Builder.io) — 3rd partyスクリプトを Web Worker で実行。
<script src="https://cdn.jsdelivr.net/npm/@builder.io/partytown/lib/partytown.js"></script>
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
Next.js Scriptコンポーネント:
import Script from 'next/script'
<Script src="https://analytics.example.com" strategy="lazyOnload" />
<Script src="https://critical.example.com" strategy="beforeInteractive" />
Long Taskとメインスレッド予算
Long Task: 50ms超でメインスレッドをブロックするJS作業。INPを壊す最大原因。
Long Task検知
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long Task:', entry.duration, 'ms', entry.attribution)
}
}).observe({ type: 'longtask', buffered: true })
Long Animation Frames (LoAF) — 2024年新規
Long Taskの限界を補う新API。フレーム単位で レンダリング + スクリプト + Layout + Paint 時間を分析。
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LoAF:', {
duration: entry.duration,
scripts: entry.scripts, // どのスクリプトが何ms取ったか
blockingDuration: entry.blockingDuration,
})
}
}).observe({ type: 'long-animation-frame', buffered: true })
scheduler.yield() — メインスレッドへ譲る
async function bulkWork(items) {
for (const item of items) {
process(item)
if (navigator.scheduling?.isInputPending()) {
await scheduler.yield() // 入力待ちがあれば即譲る
}
}
}
Web Worker — CPUオフロード
// main.js
const worker = new Worker('/worker.js')
worker.postMessage({ cmd: 'hash', data: largeString })
worker.onmessage = (e) => console.log('hashed:', e.data)
// worker.js
self.onmessage = async (e) => {
const buf = new TextEncoder().encode(e.data.data)
const hash = await crypto.subtle.digest('SHA-256', buf)
self.postMessage(Array.from(new Uint8Array(hash)))
}
Hydration問題 — SPAの根本コスト
React/Vue/AngularのようなSPAは Hydration — サーバーでレンダリングされたHTMLにJSを付けて相互作用可能にする過程 — がINPを壊す。
Hydrationの6段階コスト (Addy Osmani)
- JSダウンロード — 200–500KB gzipped
- JSパース + コンパイル
- Reactツリー再構築 (サーバーHTMLとは無関係)
- イベントリスナー付与
- useState/useEffect実行
- Commit
モバイルでこれ全体が 1–3秒 かかる。その間ユーザークリックは無視される。
解決策1: Partial Hydration (Islands)
Astro、Marko、Fresh (Deno) のアプローチ。ページの大半は静的HTML、相互作用が必要な部分のみ を島 (island) としてHydrate。
---
// Astroファイル
import Counter from './Counter.tsx'
---
<html>
<body>
<h1>静的コンテンツ (Hydrateしない)</h1>
<Counter client:visible /> <!-- ビューポート進入時Hydrate -->
<Counter client:idle /> <!-- アイドル状態のとき -->
<Counter client:load /> <!-- 即時 -->
</body>
</html>
解決策2: Resumability (Qwik)
Qwikの革新: Hydration自体を無くし、HTMLにserializeされた状態をユーザー相互作用時点で再開 (resume)。
// Qwikコンポーネント
export default component$(() => {
const count = useSignal(0)
return (
<button onClick$={() => count.value++}>
{count.value}
</button>
)
})
HTMLにハンドラURLを埋め込む:
<button on:click="app.js#Counter_onClick[0]">0</button>
JS 0KBでスタート、クリック時点で該当ハンドラJSをlazy load。TTI = LCP を実現。
解決策3: React Server Components + Streaming
React 18 + Next.js 14。Server ComponentsはJSバンドルに含まれない → クライアントバンドルが縮小。Streamingで <Suspense> 境界まで先にレンダリング → LCP短縮。
解決策4: Selective Hydration
React 18の基本機能。<Suspense> 境界を優先度ベースでHydrate。ユーザーがクリックした領域を優先処理。
HTTP/3、QUIC、そしてネットワーク層
2024年、HTTP/3は全トラフィックの 30% を超えた (W3Techs)。TCP上で動くHTTP/2と異なり、HTTP/3は UDPベースのQUICプロトコル 上で動く。
HTTP/1.1 → HTTP/2 → HTTP/3
| バージョン | 基盤 | 多重化 | Head-of-Line Blocking | 0-RTT |
|---|---|---|---|---|
| HTTP/1.1 | TCP | X (6 connections/origin) | Yes | No |
| HTTP/2 | TCP | Yes | TCPレベル Yes | No |
| HTTP/3 | UDP (QUIC) | Yes | No | Yes |
HTTP/3のコア利得:
- 0-RTT Resumption — 以前の接続鍵を再利用、初回要求からデータ送信
- Connection Migration — WiFi → セルラー切替でも接続維持 (Connection ID)
- HOL Blocking解消 — あるストリームのパケット喪失が他ストリームをブロックしない
- 暗号化必須 — TLS 1.3内蔵、平文不可
実計測 (Cloudflare 2024資料):
- Google Search: HTTP/3で中央値応答時間3%短縮、上位10%区間10%短縮
- Facebook: 動画rebuffering 20%減
- Akamai: モバイルTTFB 12%改善
CDN + Edgeコンピューティング
Cloudflare、Fastly、AWS CloudFront、Vercel Edge Network、Bunny.net。コンテンツをユーザー近傍にキャッシュして遅延を最小化。
2025年トレンド:
- Edge Workers — Cloudflare Workers、Deno Deploy、Vercel Edge Functions。V8 Isolateベースで数msのコールドスタート。
- Regional Edge Cache — 従来のOrigin → Edge 2段階を3段階に (Origin → Regional → Edge)。
- Smart Placement (Cloudflare) — ユーザーベースではなく Origin近傍 に配置してDB遅延を最小化。
2025年のパフォーマンスツールスタック
計測ツール
- Chrome DevTools Performance — 最も基本。2024年に Performance Insights パネル追加でCore Web Vitalsのリアルタイム分析。
- Lighthouse — Chrome内蔵、CI用
lighthouse-ci、Vercel/Netlify統合。 - WebPageTest — 詳細分析、Filmstrip、接続詳細。無料 + 有料プラン。
- PageSpeed Insights — Lab (Lighthouse) + Field (CrUX) 統合ビュー。
- Chrome UX Report — 月次公開、BigQueryで競合比較。
プロファイラ
- SpeedScope — https://www.speedscope.app、フレームグラフ可視化、Chrome Performanceプロファイルをインポート。
- Perfetto — Chrome DevToolsとChromium内部トレース、UI共有。
- React DevTools Profiler — コンポーネントレンダリング時間分解。
- Next.js Build Analyzer —
@next/bundle-analyzer、バンドルサイズ可視化。
RUM
- Vercel Speed Insights + Web Analytics — Next.js標準。
- Sentry Performance — エラー + RUM統合。
- New Relic Browser — APM連携。
- Cloudflare Web Analytics — 無料、プライバシー優先。
- SpeedCurve — 競合比較に強み。
最適化ツール
- Next.js Image + Vercel Image Optimization — AVIF/WebP自動。
- Sharp (Node.js) — サーバーサイド画像変換。
- Partytown — 3rd partyスクリプトWorker隔離。
- Fontaine (Vite plugin) — fallbackフォント自動生成。
- Critical (Addy Osmani) — Critical CSS抽出。
実戦最適化チェックリスト (2025)
実Webサイトを最適化する順序:
- RUM導入 — Vercel Speed Insightsまたはweb-vitalsライブラリで実指標計測
- CDN + HTTP/3 + Brotli — ネットワーク層
- サーバーTTFB 200ms以下 — DBクエリ最適化、SSRキャッシュ、Edge Function
- LCP画像最適化 — AVIF +
fetchpriority="high"+preload - Critical CSSインライン + 残りはdeferred —
media="print"ハックまたはCriticalライブラリ - Font Loading —
font-display: optional+ size-adjust fallback - JS Code Splitting — ルート単位 + React.lazy
- 3rd Party Scripts監査 — Partytown、
next/scriptstrategy="lazyOnload" - CLS除去 — 画像/iframeのwidth/height、Adスロットのaspect-ratio、フォントfallbackマッチング
- INP最適化 — Long Taskの分割 (scheduler.yield)、useTransition、Web Worker
- Speculation Rules — 予測可能な次ページをprerender
- リグレッション防止 — Lighthouse CI、Performance Budget (webpack/rollup plugin)
よくある10のアンチパターン
- LCP画像に
loading="lazy"を付与 — LCPが永続遅延。 - フォント戦略なしのカスタムフォント — FOIT 3秒、空白画面。
- React全Hydration + 静的サイト — Astro/Next SSGを使わずNext.js CSR。
- Third-partyスクリプトのblockingロード — GA/GTMをheadに素で入れる。
- クライアントでMarkdownレンダリング — サーバーで事前HTML化すべき。
- Lighthouseスコアのみモニタリング — RUMなしでは実ユーザー体験に盲目。
- Layout Thrashing —
forループ内でoffsetHeightを繰り返し読み書き。 - 巨大画像オリジナルロード — 4K画像を200pxサムネイルに使用。
- 同期
fetchをevent handlerでawait — INPが大きく悪化。 - Hydration中のstate更新 — 無限Hydration/再レンダリングループ。
次回予告 — データベースの新しい波 — PostgreSQL、pgvector、HNSW、AI時代のDB戦略
Webパフォーマンス最適化の終着点は通常 データベース だ。どれだけCDNを上手く使っても、DBクエリが遅ければTTFBが壊れる。2023–2025年のデータベース界最大の事件は PostgreSQLによるベクトルデータベース征服 だった。pgvector 拡張がPinecone、Weaviate、Qdrantのような専用ベクトルDBを脅かし、「万能DBとしてのPostgreSQL」時代を開いた。
次回は:
- PostgreSQLがなぜ再び1位か — StackOverflow 2024開発者調査1位
- pgvectorとHNSWインデックス — ベクトル検索の数学と実際
- pgvector vs Pinecone vs Weaviate vs Qdrant — 性能/機能/コスト比較
- PostgreSQL 17の飛躍 — Logical Replication、Incremental Backup
- Supabase、Neon、PlanetScale、CockroachDB — クラウドPostgreSQLエコシステム
- JSON、JSONB、GINインデックス — NoSQL機能の完全統合
- MVCC原理 — 楽観的同時実行制御の優雅さ
- Citus、TimescaleDB、PostGIS — 拡張エコシステム
- PostgreSQL + AI — RAGパイプライン実戦
を扱う。「一つのDBですべて」が現実となった時代、その背景と実戦設計を見ていく。Webパフォーマンスの旅がデータ層に続く理由を追ってみよう。