Service Worker画像プリフェッチ予算管理 2025 — Priority RulesとINP健全化の実践
公開: 2025年9月29日 · 読了目安: 6 分 · 著者: Unified Image Tools 編集部
LCP 改善のために画像プリフェッチを導入したものの、Service Worker が帯域を使いすぎて INP が悪化する、という報告が増えています。2025 年は Priority Hints の安定実装と、Network Information API の指標精度向上により、プリフェッチを動的に制御する土壌が整いました。本稿では、プリフェッチ対象とタイミングを「予算」として管理し、ユーザー体験を損なわずにヒーロー画像やギャラリーを先読みする方法を、コーダー視点でまとめます。
TL;DR
- プリフェッチ予算を数値化:
budget = (下り帯域 × 0.25) - 同時ロード中のLCP資産
として算出し、マイナスならプリフェッチを中断。 - Service Workerで優先度を再計算: Navigation Timing・INP テレメトリを収集し、次回訪問時にプリフェッチ対象の rank を更新。
- Priority Hintsとfetchpriorityの連携: HTML に仕込んだ
fetchpriority="low"
を Service Worker で上書きし、状況に応じてauto
/high
に切り替え。 - Background Syncで再試行: オフライン・低帯域時はキャンセルし、
periodicSync
で夜間に再プリフェッチ。 - Observability: performance-guardian のエッジログにプリフェッチ成功率と ΔLCP を記録し、予算が適切か継続監視。
予算モデルの設計
指標 | 取得方法 | 推奨頻度 | 目的 |
---|---|---|---|
downlink | navigator.connection.downlink | セッション開始、ネットワーク変化時 | 帯域推定 |
effectiveType | Network Information API | 毎回 | 3G/4G/5G 判定 |
inpP75 | PerformanceObserver + RUM | 毎回 | INP 劣化防止の警告トリガ |
lcpCandidateSize | performance.getEntriesByType('largest-contentful-paint') | 最初のLCP確定時 | LCP 資産のサイズ把握 |
prefetchSuccessRate | Service Worker ログ | 日次 | プリフェッチ効果の評価 |
プリフェッチは常に正解ではないため、上記の指標を基に「今は予算があるか」を判断します。
// sw/budget.ts
export function calculateBudget({ downlink, lcpSize, concurrentLoads }: {
downlink: number
lcpSize: number
concurrentLoads: number
}) {
const capacity = downlink * 125000 // Mbps -> bytes/s
const reserved = lcpSize + concurrentLoads * 150000
return Math.max(0, capacity * 0.25 - reserved)
}
プリフェッチキューの構築
プリフェッチ候補は prefetch-manifest.json
で管理します。
[
{
"id": "hero-day2",
"url": "/images/event/day2@2x.avif",
"priority": 0.9,
"type": "image",
"expectedSize": 320000
},
{
"id": "gallery-mini",
"url": "/images/gallery/thumbs.webp",
"priority": 0.4,
"type": "image",
"expectedSize": 90000
}
]
Service Worker ではこのマニフェストを読み込み、予算内に収まるものだけをキューに追加します。
// sw/prefetch.ts
import { calculateBudget } from './budget'
import manifest from '../prefetch-manifest.json'
self.addEventListener('message', event => {
if (event.data?.type !== 'INIT_PREFETCH') return
const state = event.data.state
const budget = calculateBudget({
downlink: state.downlink,
lcpSize: state.lcpSize,
concurrentLoads: state.concurrentLoads
})
const queue = manifest
.filter(item => item.expectedSize <= budget)
.sort((a, b) => b.priority - a.priority)
prefetchQueue(queue)
})
async function prefetchQueue(queue) {
for (const entry of queue) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 4000)
try {
await fetch(entry.url, {
priority: entry.priority > 0.7 ? 'high' : 'low',
signal: controller.signal
})
await caches.open('prefetch-v1').then(cache => cache.add(entry.url))
logPrefetch(entry.id, true)
} catch (error) {
logPrefetch(entry.id, false, error)
} finally {
clearTimeout(timeout)
}
}
}
fetchpriority
はまだ実験的ですが、Chrome/Safari で使用可能です。priority
オプションが未対応のブラウザでは fetchpriority
属性を書き換える fallback を実装します。
Priority HintsとHTMLの連携
// app/layout.tsx
export function PrefetchHints() {
return (
<>
<link
rel="preload"
as="image"
href="/images/event/day2@2x.avif"
fetchPriority="low"
/>
<script
dangerouslySetInnerHTML={{
__html: `navigator.serviceWorker?.controller?.postMessage({
type: 'INIT_PREFETCH',
state: {
downlink: navigator.connection?.downlink || 1.5,
lcpSize: window.__LCP_SIZE__ || 200000,
concurrentLoads: window.__IN_FLIGHT__ || 0
}
});`
}}
/>
</>
)
}
INPを守るためのキャンセル戦略
INP が悪化した場合、即座にプリフェッチを中断し、次回以降の優先度を下げます。
// sw/inp-monitor.ts
const INP_THRESHOLD = 200
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.duration > INP_THRESHOLD) {
self.registration.active?.postMessage({ type: 'CANCEL_PREFETCH' })
updatePriority(entry.eventType)
}
}
}).observe({ type: 'event', buffered: true })
CANCEL_PREFETCH
でキューを停止し、priority
を 0.1 ずつ減少させるアルゴリズムを採用すると、重いユーザー アクションが発生したページでは自動的にプリフェッチが抑制されます。
Background Sync と夜間プリフェッチ
低帯域やオフライン時にプリフェッチを強行すると、ページ操作が詰まります。periodicSync
を使って夜間や Wi-Fi 接続時に再実行します。
// sw/background-sync.ts
self.addEventListener('sync', event => {
if (event.tag !== 'prefetch-sync') return
event.waitUntil(prefetchQueue(manifest))
})
async function scheduleSync() {
const registration = await self.registration.periodicSync?.register('prefetch-sync', {
minInterval: 6 * 60 * 60 * 1000
})
return registration
}
モニタリングと分析
performance-guardian のカスタムイベントを使ってプリフェッチ効果を RUM に送信します。
sendToAnalytics('prefetch', {
budget,
downlink,
prefetchSuccessRate,
deltaLCP,
deltaINP
})
ダッシュボードでは以下の KPI をトラッキングします。
KPI | 目安 | アラート条件 |
---|---|---|
ΔLCP (プリフェッチ有無) | -180ms 程度 | 正の値が 3 日継続 |
INP p75 | < 180ms | 200ms 超で即停止 |
Prefetch Success Rate | > 85% | 70% 未満でマニフェスト再調整 |
帯域占有率 | < 30% | 50% 超で AB テスト停止 |
チェックリスト
- [ ]
prefetch-manifest.json
が PR レビュー対象になっている - [ ]
calculateBudget
のパラメータが A/B テストで検証済み - [ ] INP 悪化時にプリフェッチが即停止する
- [ ] Background Sync で Wi-Fi 時のみ再プリフェッチする
- [ ] [performance-guardian](/ja/tools/performance-guardian) で ΔLCP, ΔINP がダッシュボード化されている
- [ ] CDN のキャッシュヒット率がプリフェッチ導入前後で比較されている
まとめ
Service Worker による画像プリフェッチは、無計画に導入すると帯域と INP を圧迫します。予算モデル、優先度の動的再計算、Background Sync の活用によってプリフェッチを状況依存にし、ユーザー体験を守りながら LCP を改善しましょう。Web コーダーは、ブラウザ API と CI 実装を組み合わせて監視・制御ループを自動化し、サイトごとの最適なプリフェッチ戦略を育てていく必要があります。
関連ツール
関連記事
損失管理型ストリーミングスロットリング 2025 — AVIF/HEIC帯域制御と品質SLO
AVIF/HEICなどの高圧縮フォーマットを配信する際に、帯域スロットリングと品質SLOを両立させるためのストリーミング制御と監視手法を整理。
WASMビルドパイプラインで画像最適化を自動化 2025 — esbuildとLightning CSSの統合レシピ
WASM対応ビルドチェーンで画像の派生生成・検証・署名を自動化する実装パターン。esbuild、Lightning CSS、Squoosh CLI を統合し、CI/CDで耐障害性を確保する方法を整理。
画像圧縮 完全戦略 2025 ─ 画質を守りつつ体感速度を最適化する実践ガイド
Core Web Vitals と実運用に効く最新の画像圧縮戦略を、用途別の具体的プリセット・コード・ワークフローで徹底解説。JPEG/PNG/WebP/AVIF の使い分け、ビルド/配信最適化、トラブル診断まで網羅。
CDNサービスレベル監査 2025 — 画像配信SLAを可視化する監査基盤
マルチCDN環境で画像SLAを証明するための監査アーキテクチャ。計測指標、証跡収集、ベンダー交渉に使えるレポーティング手法を紹介。
Core Web Vitals 実践モニタリング 2025 — エンタープライズ案件のSREチェックリスト
エンタープライズ規模のWeb制作チームがCore Web Vitalsを継続監視するためのSRE運用テンプレート。SLO設計、メトリクス収集、インシデント対応まで包括的に解説します。
Edge画像配信オブザーバビリティ 2025 — Web制作会社のSLO設計と運用手順
Edge CDNとブラウザでの画像配信品質を観測するためのSLO設計、計測ダッシュボード、アラート運用をWeb制作会社向けに詳解。Next.jsとGraphQLを使った実装例付き。