modern-react-spa

Chapter 33

Network Performance

Network performance for React SPAs — HTTP/3 awareness, React 19.2 resource hints (preload, preinit), image strategy, and service workers for offline-first.

Published 2026-05-23

🎯 Chapter Goal — After this chapter you can tune the network layer of a React SPA: HTTP/3 awareness, resource hints (preload/preinit from React 19.2), image strategy that doesn’t cost 60 % of your page weight, and service workers for offline-first when the use case justifies it.

🧭 Prerequisites — Ch 2 §2.5 (asset loading APIs), Ch 17 (Vite), Ch 30 (budget).


🔹 33.1 HTTP/3 and resource hints

HTTP/3 runs over QUIC (UDP) instead of TCP. Universal in 2026 across major CDNs (Cloudflare, Fastly, Akamai, AWS CloudFront). For most SPAs, you get HTTP/3 for free by deploying behind a modern CDN; no app code change.

What changes: head-of-line blocking is gone. Multiple requests over one connection no longer wait on each other. The practical implication: aggressive per-request splitting is cheaper than under HTTP/1.1.

Resource hints — preload, preinit, prefetchDNS, preconnect

React 19.2 ships these as JSX-callable functions (Ch 2 §2.5). The mental model:

prefetchDNS('https://api.acme.example')        ← cheap; do early
preconnect('https://cdn.acme.example')         ← DNS + TCP + TLS handshake
preload('/img/hero.webp', { as: 'image' })     ← fetch, don't apply
preinit('/css/route.css', { as: 'style' })     ← fetch and apply

Where to call them:

import { prefetchDNS, preconnect, preinit } from 'react-dom';

export const AppShell = ({ children }: { children: React.ReactNode }) => {
  prefetchDNS('https://api.acme.example');
  preconnect('https://cdn.acme.example', { crossOrigin: 'anonymous' });
  preinit('/fonts/inter.woff2', { as: 'font', crossOrigin: 'anonymous' });

  return <div className="shell">{children}</div>;
};

React-DOM dedupes. Calling prefetchDNS from every route is fine — one hint per host.

The waterfall difference is dramatic for first paint (see Ch 2 §2.5 mock).


🔹 33.2 Image strategy

Images often dominate page weight. Three layers:

Format

FormatWhen to use
AVIFModern browsers; best compression
WebPWide support; fallback for old browsers
JPEGLegacy fallback
PNGOnly when transparency + lossless required
SVGVector content (icons, logos, illustrations)

Generate AVIF + WebP at build time; serve via <picture>:

<picture>
  <source srcSet="/hero.avif" type="image/avif" />
  <source srcSet="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="…" width={1200} height={600} />
</picture>

Always include width / height (prevents CLS).

Responsive srcset

Serve smaller images to smaller viewports:

<img
  src="/hero-800.jpg"
  srcSet="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1600.jpg 1600w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1600px"
  alt="…"
  width={1600} height={800}
/>

The browser picks the best file for the device’s screen + DPR. A phone user doesn’t download the 1 600 px hero.

loading="lazy"

Native lazy-loading. Browser-supported everywhere in 2026:

<img src="/below-fold.jpg" loading="lazy" alt="…" />

Apply to anything below the fold. Save the initial-paint network for the above-fold images.

⚠️ Don’t lazy-load above-the-fold images. Browser delays the fetch; LCP suffers.

Image CDNs

For dynamic image needs (user-uploaded content, on-the-fly resizing), an image CDN (Cloudflare Images, imgix, Cloudinary) generates the right variant per request. Often cheaper than building a build-time pipeline.


🔹 33.3 Service workers and offline-first

Service workers cache responses and serve them from disk on subsequent visits — including offline.

When to add one:

  • Users actually go offline (mobile, in-the-field tools, embedded devices).
  • Predictable repeat-visitor patterns where caching app shells / API responses helps.
  • You’re shipping a PWA installable to the home screen.

When NOT to add one:

  • Public marketing site — users hit once, no benefit from SW caching, you pay debugging cost.
  • Heavily-personalised app — SW caching the wrong user’s data is a security incident.
  • Team without service-worker experience — SWs are subtle (cache lifecycle, message channels, scoping rules).

Workbox — the canonical library

https://web.dev/workbox/ — Google’s SW toolkit. Recipes for common patterns:

// sw.ts (generated via vite-plugin-pwa)
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';

precacheAndRoute(self.__WB_MANIFEST);   // pre-cache the app shell

registerRoute(
  ({ url }) => url.origin === 'https://cdn.acme.example',
  new CacheFirst(),                      // CDN assets: cache forever
);

registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ networkTimeoutSeconds: 3 }),  // API: prefer fresh, fall back to cache
);

vite-plugin-pwa wires the generation into Vite. Most teams should use it instead of hand-rolling a SW.

The update flow

When a new SW deploys, the old one keeps serving until the user closes every tab. Pattern: prompt the user to refresh on update.

// in your app
if ('serviceWorker' in navigator) {
  const reg = await navigator.serviceWorker.register('/sw.js');
  reg.addEventListener('updatefound', () => {
    const sw = reg.installing!;
    sw.addEventListener('statechange', () => {
      if (sw.state === 'installed' && navigator.serviceWorker.controller) {
        // new version installed; old still active
        showUpdateBanner(() => sw.postMessage({ type: 'SKIP_WAITING' }));
      }
    });
  });
}

The banner says “New version available — refresh to update.” User clicks; new SW activates; page reloads.

⚠️ A bad SW that you’ve shipped can be hard to remove. Practice the recovery path: ship a SW that unregisters itself, deploy, wait, then ship the real new SW.

🪤 Common Pitfalls

  1. Adding a service worker without a recovery plan — you can’t undo a bad one easily.
  2. Lazy-loading above-the-fold images → LCP regression.
  3. Missing width/height on <img> → CLS.
  4. JPEG when AVIF/WebP would have been 60 % smaller.
  5. Per-route <link rel="stylesheet"> blocking render → use preinit.
  6. preload for every image on the page — defeats the purpose; only hint what you’ll need imminently.
  7. Service worker that caches authenticated API responses → users see other users’ data.

✅ Recap

  • HTTP/3 is free behind a modern CDN; design for many small chunks over one connection.
  • Resource hints (Ch 2 §2.5) move the network waterfall left.
  • AVIF/WebP + responsive srcset + loading="lazy" is the 2026 image baseline.
  • Service workers are powerful but subtle — adopt deliberately, with a recovery plan.

🔗 Further Reading

In the book — not on the site

Each topic has an 🧠 Under-the-hood subsection — the algorithm, the data structures, what React DevTools surfaces, debugging recipes. Plus a 🧪 hands-on lab per chapter with a starter repo. Reserved for the book.

Topics in this chapter (3)
  1. 33.1 HTTP/3 and resource hints
  2. 33.2 Image strategy
  3. 33.3 Service workers and offline-first