Service Worker Image Prefetch Budgeting 2025 — Practicals for Priority Rules and Healthy INP
Published: Sep 29, 2025 · Reading time: 5 min · By Unified Image Tools Editorial
Teams often add image prefetching for better LCP only to find their Service Worker hogs bandwidth and pushes INP in the wrong direction. In 2025 we finally have stable Priority Hints and higher-fidelity Network Information API signals, letting us modulate prefetching dynamically. This article frames target assets and timing as a “budget,” showing how to preload hero images or galleries without eroding user experience.
TL;DR
- Quantify the budget: compute
budget = (downlink × 0.25) - in-flight LCP assets
; pause prefetch when it dips below zero. - Re-rank inside the Service Worker: capture Navigation Timing + INP telemetry and adjust next-visit prefetch rankings automatically.
- Bridge Priority Hints and
fetchpriority
: override the HTML default (low
) from the Service Worker, shifting toauto
/high
when conditions warrant. - Retry with Background Sync: cancel during offline or constrained sessions and reschedule via
periodicSync
overnight. - Observability: log prefetch success and ΔLCP to performance-guardian edge events to monitor whether the budget is tuned.
Designing the Budget Model
Metric | How to capture | Recommended cadence | Purpose |
---|---|---|---|
downlink | navigator.connection.downlink | Session start & network changes | Bandwidth estimate |
effectiveType | Network Information API | Every run | 3G/4G/5G classification |
inpP75 | PerformanceObserver + RUM | Every run | Trigger when INP degrades |
lcpCandidateSize | performance.getEntriesByType('largest-contentful-paint') | When LCP finalises | Understand LCP asset size |
prefetchSuccessRate | Service Worker logs | Daily | Evaluate prefetch impact |
Prefetching is situational; use these signals to decide whether budget exists right now.
// 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)
}
Building the Prefetch Queue
Manage candidates in 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
}
]
The Service Worker loads this manifest and queues only what fits inside the current budget.
// 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
is still experimental but runs in Chrome/Safari. For browsers without the priority
option, implement a fallback that rewrites the <link fetchpriority>
attribute.
Wiring Priority Hints to 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
}
});`
}}
/>
</>
)
}
Cancel Strategy to Protect INP
When INP drifts upward, stop prefetching immediately and lower future priorities.
// 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
halts the current queue; decrementing priority
by 0.1 each time lets heavy interaction pages suppress prefetch naturally.
Background Sync and Overnight Prefetch
Forcing prefetching on poor connections stalls the UI. Use periodicSync
to retry on Wi-Fi or during off-hours.
// 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
}
Monitoring and Analytics
Send RUM events to performance-guardian to track whether prefetching helps.
sendToAnalytics('prefetch', {
budget,
downlink,
prefetchSuccessRate,
deltaLCP,
deltaINP
})
Key dashboards should include:
KPI | Target | Alert condition |
---|---|---|
ΔLCP (prefetch vs none) | ≈ -180ms | Positive values for 3 consecutive days |
INP p75 | < 180ms | > 200ms → stop immediately |
Prefetch Success Rate | > 85% | < 70% → rework manifest |
Bandwidth consumption | < 30% | > 50% → pause experiment |
Checklist
- [ ]
prefetch-manifest.json
is code-reviewed. - [ ]
calculateBudget
parameters are AB-tested. - [ ] Prefetch halts instantly when INP worsens.
- [ ] Background Sync retries only on Wi-Fi.
- [ ] performance-guardian dashboards ΔLCP/ΔINP.
- [ ] CDN cache hit rate is compared pre/post rollout.
Summary
Service Worker prefetching can backfire when unbudgeted, consuming bandwidth and hurting INP. By layering a budget model, dynamic prioritisation, and Background Sync, you can conditionally prefetch; LCP improves while the user experience stays smooth. Engineers should pair browser APIs with CI automation to create monitoring and control loops, then evolve the prefetch strategy per site.
Related tools
Performance Guardian
Model latency budgets, track SLO breaches, and export evidence for incident reviews.
Compare Slider
Intuitive before/after comparison.
Image Quality Budgets & CI Gates
Model ΔE2000/SSIM/LPIPS budgets, simulate CI gates, and export guardrails.
Audit Logger
Log remediation events across image, metadata, and user layers with exportable audit trails.
Related Articles
Loss-aware streaming throttling 2025 — AVIF/HEIC bandwidth control with quality SLOs
A field guide to balancing bandwidth throttling and quality SLOs when delivering high-compression formats like AVIF/HEIC. Includes streaming control patterns, monitoring, and rollback strategy.
Automating Image Optimization with a WASM Build Pipeline 2025 — A Playbook for esbuild and Lightning CSS
Patterns for automating derivative image generation, validation, and signing with a WASM-enabled build chain. Shows how to integrate esbuild, Lightning CSS, and Squoosh CLI to achieve reproducible CI/CD.
Image Delivery Optimization 2025 — Priority Hints / Preload / HTTP/2 Guide
Image delivery best practices that don't sacrifice LCP and CLS. Combine Priority Hints, Preload, HTTP/2, and proper format strategies to balance search traffic and user experience.
Image Priority Design and Preload Best Practices 2025
Correctly apply fetchpriority and preload to LCP candidate images. Learn imagesrcset/sizes usage, preload pitfalls, and implementation that doesn't harm INP with practical examples.
CDN Service Level Auditor 2025 — Evidence-Driven SLA Monitoring for Image Delivery
Audit architecture for proving image SLA compliance across multi-CDN deployments. Covers measurement strategy, evidence collection, and negotiation-ready reporting.
Core Web Vitals Practical Monitoring 2025 — SRE Checklist for Enterprise Projects
An SRE-oriented playbook that helps enterprise web production teams operationalize Core Web Vitals, covering SLO design, data collection, and incident response end to end.