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 to auto/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

MetricHow to captureRecommended cadencePurpose
downlinknavigator.connection.downlinkSession start & network changesBandwidth estimate
effectiveTypeNetwork Information APIEvery run3G/4G/5G classification
inpP75PerformanceObserver + RUMEvery runTrigger when INP degrades
lcpCandidateSizeperformance.getEntriesByType('largest-contentful-paint')When LCP finalisesUnderstand LCP asset size
prefetchSuccessRateService Worker logsDailyEvaluate 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:

KPITargetAlert condition
ΔLCP (prefetch vs none)≈ -180msPositive 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 Articles

Compression

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.

Workflow

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.

Web

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.

Web

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.

Web

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.

Web

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.