Cache-First Strategy — Fastest Loading for Static Assets
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
- 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.
- 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.
- Forgetting to update cached assets. With simple cache-first, cached assets never update. Add background revalidation or version your cache.
- Caching the entire site during install. Pre-cache only critical assets. Cache other pages on demand as users visit them.
- Not handling network failures gracefully. When both cache and network fail, users see an error. Provide an offline fallback.
Practice Questions
- What types of resources benefit most from cache-first strategy?
- How does cache-first differ from network-first in terms of user experience?
- Why should you avoid cache-first for API responses?
- How does versioned file naming make cache-first more effective?
- 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
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