Gestion du budget de préchargement d’images via Service Worker 2025 — Priorisation et INP maîtrisé

Publié: 29 sept. 2025 · Temps de lecture: 5 min · Par la rédaction Unified Image Tools

De nombreuses équipes ajoutent du préchargement d’images pour améliorer le LCP et se retrouvent avec un Service Worker qui monopolise la bande passante et détériore l’INP. En 2025, Priority Hints est stabilisé et le Network Information API fournit des signaux plus fiables, ce qui permet de moduler le préchargement dynamiquement. Cet article traite la sélection d’assets et leur timing comme un « budget », et montre comment anticiper les héros ou galeries sans sacrifier l’expérience utilisateur.

TL;DR

  • Quantifier le budget : calculer budget = (downlink × 0,25) - ressources LCP en cours, puis interrompre le préchargement quand le résultat devient négatif.
  • Reclasser côté Service Worker : collecter Navigation Timing + télémétrie INP et ajuster automatiquement le rang pour la prochaine visite.
  • Relier Priority Hints à fetchpriority : écraser la valeur HTML (low) depuis le Service Worker et basculer vers auto/high selon le contexte.
  • Réessayer avec Background Sync : annuler en connexion limitée/offline et replanifier via periodicSync la nuit.
  • Observabilité : tracer taux de succès et ΔLCP dans performance-guardian pour vérifier que le budget reste pertinent.

Concevoir le modèle de budget

MétriqueCollecteCadence conseilléeObjectif
downlinknavigator.connection.downlinkDébut de session & changements réseauEstimation de bande passante
effectiveTypeNetwork Information APIÀ chaque exécutionQualification 3G/4G/5G
inpP75PerformanceObserver + RUMÀ chaque exécutionDéclencher si l’INP se dégrade
lcpCandidateSizeperformance.getEntriesByType('largest-contentful-paint')Quand le LCP se figeMesurer la taille de la ressource LCP
prefetchSuccessRateLogs Service WorkerQuotidienÉvaluer l’efficacité du préchargement

Le préchargement reste circonstanciel ; basez-vous sur ces signaux pour décider si un budget existe à l’instant T.

// 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)
}

Construire la file de préchargement

Les candidats sont décrits dans 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
  }
]

Le Service Worker lit le manifeste et ne met en file que ce qui tient dans le budget courant.

// 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 reste expérimental mais fonctionne déjà dans Chrome/Safari. Pour les navigateurs sans support de l’option priority, implémentez un repli qui réécrit l’attribut <link fetchpriority>.

Orchestrer Priority Hints et 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
            }
          });`
        }}
      />
    </>
  )
}

Stratégie d’annulation pour protéger l’INP

Dès que l’INP augmente, stoppez le préchargement et diminuez la priorité pour les visites suivantes.

// 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 interrompt la file courante ; décrémenter priority de 0,1 permet aux pages riches en interactions de restreindre d’elles-mêmes le préchargement.

Background Sync et préchargement nocturne

Forcer le préchargement sur une connexion faible bloque l’interface. Utilisez periodicSync pour réessayer en Wi-Fi ou en heures creuses.

// 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 et analytics

Envoyez des événements RUM vers performance-guardian pour mesurer l’apport du préchargement.

sendToAnalytics('prefetch', {
  budget,
  downlink,
  prefetchSuccessRate,
  deltaLCP,
  deltaINP
})

Tableau de bord recommandé :

KPICibleCondition d’alerte
ΔLCP (avec vs sans préchargement)≈ -180 msValeurs positives 3 jours d’affilée
INP p75< 180 ms> 200 ms → arrêt immédiat
Taux de succès du préfetch> 85 %< 70 % → revoir le manifeste
Occupation de bande passante< 30 %> 50 % → suspendre l’expérience

Checklist

  • [ ] prefetch-manifest.json est soumis à revue de code.
  • [ ] Les paramètres de calculateBudget sont validés via tests A/B.
  • [ ] Le préfetch s’arrête instantanément quand l’INP se dégrade.
  • [ ] Background Sync ne relance qu’en Wi-Fi.
  • [ ] performance-guardian suit ΔLCP / ΔINP.
  • [ ] Le taux de hit CDN est comparé avant/après déploiement.

En résumé

Un préchargement non budgété peut consommer la bande passante et détériorer l’INP. En combinant modèle de budget, priorisation dynamique et Background Sync, vous conditionnez le préfetch et améliorez le LCP tout en préservant l’expérience. Associez APIs navigateur et automatisation CI pour bâtir des boucles de contrôle, puis faites évoluer la stratégie selon chaque site.

Articles liés

Compression

Modération de streaming sensible aux pertes 2025 — Piloter la bande passante AVIF/HEIC avec des SLO de qualité

Guide terrain pour équilibrer limitation de bande passante et SLO de qualité lors de la diffusion de formats très compressés comme AVIF/HEIC. Couvre les schémas de contrôle, la supervision et la stratégie de rollback.

Flux de travail

Automatiser l’optimisation d’images avec un pipeline WASM 2025 — Intégrer esbuild et Lightning CSS

Modèles pour automatiser la génération, la validation et la signature de dérivés d’images avec une chaîne de compilation compatible WASM. Montre comment combiner esbuild, Lightning CSS et Squoosh CLI pour obtenir un CI/CD reproductible.

Compression

Stratégie complète de compression d'images 2025 — Guide pratique pour optimiser la vitesse perçue tout en préservant la qualité

Stratégies de compression d'images de pointe efficaces pour Core Web Vitals et l'exploitation réelle, avec des presets spécifiques par usage, du code et des flux de travail expliqués en détail. Couvre la distinction JPEG/PNG/WebP/AVIF, l'optimisation build/livraison et le diagnostic de dépannage.

Web

Auditeur de niveaux de service CDN 2025 — Surveiller les SLA d’image avec des preuves concrètes

Architecture d’audit pour démontrer le respect des SLA d’image dans un contexte multi-CDN. Inclut stratégie de mesure, collecte d’éléments probants et reporting prêt pour la négociation.

Web

Monitoring pratique des Core Web Vitals 2025 — Checklist SRE pour projets enterprise

Un playbook orienté SRE qui aide les équipes de production web enterprise à industrialiser les Core Web Vitals, du design SLO à la collecte de données et à la réponse aux incidents.

Compression

Observabilité de la diffusion d’images Edge 2025 — Guide SLO et opérations pour agences web

Détaille la conception des SLO, les tableaux de bord de mesure et l’exploitation des alertes pour suivre la qualité de diffusion des images via les CDN Edge et navigateurs, avec exemples Next.js et GraphQL adaptés aux agences web.