Back to Blog
PerformanceJune 30, 20264 min read

Service Workers and TTFB Optimization: A Guide to Cache Warming

Master Service Workers and TTFB optimization to eliminate latency. Learn how to use background synchronization and prefetching to keep your cache warm.

Service WorkersWeb PerformanceTTFBCacheFrontend EngineeringPerformanceWeb VitalsFrontend

Last month, I spent about three days debugging why our returning users were seeing a "flash of loading" despite having a perfectly configured cache-control policy. It turned out our TTFB optimization strategy was too passive; we were waiting for the browser to request resources before doing anything.

If you’re relying solely on standard browser caches, you’re missing the chance to make your application feel truly instantaneous. By moving toward predictive cache warming, you can ensure that the critical assets are already in the Cache Storage API before the user even clicks a link.

Rethinking TTFB Optimization with Service Workers

Most developers approach TTFB optimization by tweaking server responses or using DNS prefetching. While those are necessary, they are reactive. A Service Worker, however, runs in the background, independent of the main thread, allowing you to intercept network requests and serve cached content immediately.

We first tried using standard fetch() calls inside a simple onmouseover event to warm the cache. It broke because it triggered too many simultaneous requests, causing network congestion and actually increasing our LCP for the current page. We needed a smarter way to handle resource prefetching.

The solution was to decouple the prefetching logic from the UI thread. By using the Background Sync API or a simple message channel between the main thread and the Service Worker, we could offload the cache-warming burden.

The Architecture of Predictive Warming

To get this right, we need a flow that prioritizes the current page while scheduling the background cache updates for secondary routes.

Flow diagram: Main Thread → Post Message Service Worker; Service Worker → Cache Exists?; C -- No → Fetch & Cache; C -- Yes → Ignore; Fetch & Cache → Update Cache Storage

When a user navigates to our dashboard, the main thread sends a list of critical assets for the "Settings" and "Profile" pages to the Service Worker. The Service Worker checks the CacheStorage instance. If the assets aren't there, it fetches them in the background.

Why Service Workers Win

The beauty of using Service Workers for this is that the cache stays warm across sessions. Unlike localStorage or standard HTTP caching, which can be cleared or ignored, the Service Worker cache is persistent until you explicitly purge it.

StrategyLatency ImpactImplementation Complexity
Standard HTTP CachingMediumLow
Core Web Vitals OptimizationHighMedium
Speculation Rules APIHighMedium
Service Worker WarmingVery HighHigh

Implementing the Background Sync

You don't need a complex framework to get started. Here is a basic implementation of a message-based cache warmer:

JAVASCRIPT
// Inside your Service Worker (sw.js)
self.addEventListener(CE9178">'message', (event) => {
  if (event.data.action === CE9178">'warm-cache') {
    const assets = event.data.urls;
    event.waitUntil(
      caches.open(CE9178">'app-assets-v1').then((cache) => {
        return cache.addAll(assets);
      })
    );
  }
});

// Inside your main application code
if (CE9178">'serviceWorker' in navigator) {
  navigator.serviceWorker.controller.postMessage({
    action: CE9178">'warm-cache',
    urls: [CE9178">'/js/profile-bundle.js', CE9178">'/css/settings.css']
  });
}

This approach allows you to control exactly when the prefetching happens. You might trigger it after the window.onload event to ensure the current page is fully interactive before consuming bandwidth.

Avoiding Over-Optimization

One trap I fell into was trying to prefetch everything. We ended up bloating the cache with assets that were never used, which increased our storage footprint and occasionally led to serving stale data.

Before diving into Speculation Rules API or complex Service Worker logic, ensure your basic asset strategy is sound. If your cache-control headers are misconfigured, no amount of prefetching will save your TTFB.

Also, remember that Next.js App Router Server-Side Prefetching already handles some of this natively. Don't build a custom Service Worker implementation if your framework of choice provides a robust alternative out of the box.

FAQ

Does prefetching hurt data usage for mobile users? It can. Always check navigator.connection.saveData before triggering background fetches. If the user is on a slow connection or has data-saver mode enabled, skip the cache warming.

How do I handle cache invalidation? Use versioned cache names (e.g., app-assets-v2). When the Service Worker installs, it can delete old caches using caches.keys() and caches.delete().

Can Service Workers prefetch cross-origin resources? Yes, but you must ensure the cross-origin server supports CORS and that the response is opaque or correctly configured for your cache strategy.

I’m still experimenting with how to balance "aggressive prefetching" against the user's actual navigation patterns. Sometimes the most effective optimization isn't more code, but simply better analytics on what users actually click next. Start small, measure the impact on your real-user monitoring (RUM) data, and only scale up the complexity when you have the metrics to justify it.

Similar Posts