Skip to content

Cache-First Strategy — Fastest Loading for Static Assets

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Cache. We cover key concepts, practical examples, and best practices to help you master this topic.

The cache-first strategy serves content from the browser cache first, falling back to the network only when the resource is not cached — ideal for static assets that do not change frequently.

What You'll Learn

By the end of this tutorial, you will understand when to use cache-first, how to implement it, how it affects performance, and what types of resources benefit most from this strategy.

Why It Matters

Cache-first delivers the fastest possible load time because it eliminates network latency entirely for cached resources. For assets that do not change between visits (CSS, JavaScript, fonts, logos), cache-first reduces load time from hundreds of milliseconds to nearly zero.

Real-World Use

A documentation PWA uses cache-first for all Markdown-rendered HTML pages. When a user navigates to a previously visited page, it renders instantly from cache. Background revalidation updates the cache silently. Users experience sub-100ms page transitions.

How Cache-First Works

Cache-First Flow
    Request comes in
         ↓
    ╔═══════════════════════╗
    ║  Check cache          ║
    ╚═══════════════════════╝
         ↓
    ┌─────────┐             ┌──────────────┐
    │  Found  │             │  Not Found   │
    └────┬────┘             └──────┬───────┘
         ↓                         ↓
    Return cached           Fetch from network
    response                       ↓
                              ╔══════════════════╗
                              ║  Cache response  ║
                              ║  for next time    ║
                              ╚══════════════════╝
                                    ↓
                              Return network
                              response

Think of cache-first like checking your refrigerator before going to the grocery store. If you already have milk (cached), you use it immediately. If you are out, you go to the store (network) and put the new milk in the fridge (cache) for next time.

Basic Cache-First Implementation

self.addEventListener('fetch', event => {
    // Apply cache-first only to GET requests
    if (event.request.method !== 'GET') return;

    event.respondWith(
        caches.match(event.request)
            .then(cachedResponse => {
                if (cachedResponse) {
                    console.log('Cache-First: HIT', event.request.url);
                    return cachedResponse;
                }

                console.log('Cache-First: MISS', event.request.url);
                return fetch(event.request).then(networkResponse => {
                    // Cache the response for future visits
                    if (networkResponse.ok) {
                        const clone = networkResponse.clone();
                        caches.open('static-cache').then(cache => {
                            cache.put(event.request, clone);
                        });
                    }
                    return networkResponse;
                });
            })
            .catch(error => {
                console.error('Cache-First: Error', error.message);
                return new Response('Network error', { status: 503 });
            })
    );
});

Output:

Cache-First: HIT https://example.com/styles/main.css
Cache-First: MISS https://example.com/images/hero.jpg
Cache-First: HIT https://example.com/scripts/app.js

Cache-First with Revalidation

The basic cache-first strategy never updates cached resources automatically. Add background revalidation to keep content fresh while maintaining fast loads:

self.addEventListener('fetch', event => {
    if (event.request.method !== 'GET') return;

    event.respondWith(
        caches.match(event.request).then(cachedResponse => {
            // Start network fetch in background
            const fetchPromise = fetch(event.request)
                .then(networkResponse => {
                    if (networkResponse.ok) {
                        const clone = networkResponse.clone();
                        caches.open('static-cache').then(cache => {
                            cache.put(event.request, clone);
                        });
                        console.log('Cache updated:', event.request.url);
                    }
                    return networkResponse;
                })
                .catch(() => {
                    // Network failed, that is ok, we have cache
                    return cachedResponse;
                });

            // Return cached immediately if available
            if (cachedResponse) {
                console.log('Cache-First (revalidate): HIT', event.request.url);
                // Still trigger background update
                return cachedResponse;
            }

            // Not cached, wait for network
            console.log('Cache-First (revalidate): MISS', event.request.url);
            return fetchPromise;
        })
    );
});

Cache-First for App Shell

The app shell pattern uses cache-first for the core application UI:

const APP_SHELL_URLS = [
    '/',
    '/index.html',
    '/styles/core.css',
    '/styles/theme.css',
    '/scripts/vendor.js',
    '/scripts/main.js',
    '/images/logo.svg',
    '/fonts/inter-var.woff2'
];

// Pre-cache app shell during install
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('app-shell-v1').then(cache => {
            return cache.addAll(APP_SHELL_URLS);
        }).then(() => self.skipWaiting())
    );
});

// Serve app shell with cache-first
self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    // Only apply cache-first to app shell assets
    if (APP_SHELL_URLS.includes(url.pathname)) {
        event.respondWith(
            caches.match(event.request).then(cached => {
                if (cached) {
                    console.log('App shell: serving from cache');
                    return cached;
                }
                console.log('App shell: fetching and caching');
                return fetch(event.request).then(response => {
                    const clone = response.clone();
                    caches.open('app-shell-v1').then(cache => {
                        cache.put(event.request, clone);
                    });
                    return response;
                });
            })
        );
    }
});

When to Use Cache-First

Cache-first is best for:

  • Static assets: CSS, JavaScript, fonts, icons, logos
  • Versioned files: Files with hash in filename (style.a1b2c3.css)
  • App shell: The core HTML, CSS, and JS that render your UI
  • Images that rarely change: Logo, background images, UI icons
  • Font files: Web fonts that are part of your design system

Do NOT use cache-first for:

  • API responses: Data that changes frequently
  • User-specific content: Profiles, dashboards, settings
  • Real-time data: Stock prices, weather, chat messages
  • Authentication pages: Login, signup, password reset

Cache-First with Versioned Assets

// With versioned file names, caches can be aggressive
// since new versions get new URLs automatically

self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    // Check if file has version hash in name (e.g., main.a1b2c3.js)
    const hasVersionHash = /[a-f0-9]{8,}-|-[a-f0-9]{8,}/.test(url.pathname);

    if (hasVersionHash || url.pathname.match(/\.(css|js|woff2?)$/)) {
        // Aggressive cache-first: never go to network if cached
        event.respondWith(
            caches.match(event.request).then(cached => {
                return cached || fetch(event.request).then(response => {
                    const clone = response.clone();
                    caches.open('versioned-assets').then(cache => {
                        cache.put(event.request, clone);
                    });
                    return response;
                });
            })
        );
    }
});

Performance Impact

Cache-first eliminates network latency for cached resources. The impact is measurable:

// Measure cache-first performance
async function measureCachePerformance(url) {
    const start = performance.now();

    const response = await caches.match(url);
    if (response) {
        const elapsed = performance.now() - start;
        console.log(`Cache hit: ${url} (${elapsed.toFixed(2)}ms)`);
        return elapsed;
    }

    const fetchStart = performance.now();
    const networkResponse = await fetch(url);
    const fetchElapsed = performance.now() - fetchStart;
    console.log(`Cache miss: ${url} (${fetchElapsed.toFixed(2)}ms)`);
    return fetchElapsed;
}

// Typical results on a fast connection:
// Cache hit: /styles/main.css (0.54ms)
// Cache miss: /styles/main.css (235.12ms)

Common Mistakes

  1. Using cache-first for API data. Users get stale data because the cache is never updated until the service worker changes. Use stale-while-revalidate for APIs.
  2. Not pre-Caching during install. Cache-first requires the cache to be populated before it is useful. Without pre-caching, every first visit is a cache miss.
  3. Forgetting to update cached assets. With simple cache-first, cached assets never update. Add background revalidation or version your cache.
  4. Caching the entire site during install. Pre-cache only critical assets. Cache other pages on demand as users visit them.
  5. Not handling network failures gracefully. When both cache and network fail, users see an error. Provide an offline fallback.

Practice Questions

  1. What types of resources benefit most from cache-first strategy?
  2. How does cache-first differ from network-first in terms of user experience?
  3. Why should you avoid cache-first for API responses?
  4. How does versioned file naming make cache-first more effective?
  5. What happens to cached resources when you deploy a new version of your app?

Challenge: Implement cache-first for an app shell of 10 assets. Add background revalidation that updates the cache silently. Verify with DevTools that cache hits return in under 2ms while network requests take 200ms+.

FAQ

Does cache-first work for navigation requests?

Yes, but you must pre-cache the HTML pages during install. Without pre-caching, the first navigation is always a cache miss.

How do I update cached assets with cache-first?

Use background revalidation (fetch + update cache while returning cached) or version your cache and update the service worker to trigger a new install.

What happens if the cached response is corrupt?

If the cache returns a corrupt response, the page breaks. Validate responses before caching and provide error handling to fall back to the network.

Can I combine cache-first with other strategies?

Yes. Use cache-first for static assets and network-first for APIs. Different request types can use different strategies within the same service worker.

How much faster is cache-first than network-first?

Cache hits return in 0.5-2ms (memory cache) or 5-10ms (disk cache). Network requests typically take 100-500ms. The speedup is 50-500x.

Mini Project

Build a service worker that uses cache-first for all CSS, JS, and font files. Pre-cache the 5 most critical assets during install. Add background revalidation that updates the cache when a newer version of the asset is available. Test by loading a page, checking DevTools for cache hits, and verifying updated assets after a server change.

What's Next

You have mastered cache-first. Now learn the network-first strategy, which prioritizes fresh content while using the cache as a fallback.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro