Precaching — Strategies for Caching Resources at Install Time
In this tutorial, you will learn about Precaching. We cover key concepts, practical examples, and best practices to help you master this topic.
Precaching caches critical resources during service worker installation, ensuring your PWA works offline from the moment it activates with no network dependency after the initial visit.
What You'll Learn
By the end of this tutorial, you will understand what to pre-cache, how to generate precache manifests automatically, how Workbox handles precaching, and strategies for optimizing precache size and performance.
Why It Matters
Precaching determines the baseline offline experience of your PWA. Pre-cache too little and the app breaks offline. Pre-cache too much and the install takes too long, frustrating users on slow connections. Getting precaching right is the most important Caching decision you will make.
Real-World Use
A flight booking PWA pre-caches the app shell, search form CSS/JS, and terms and conditions. It does NOT pre-cache flight results (those are cached dynamically). Install completes in under 2 seconds on 3G. Users can access the search form offline to prepare their trip before heading to the airport.
What to Pre-Cache vs Dynamic Cache
Precache vs Dynamic Cache
┌──────────────────────────────────────────────────────────┐
│ Should I Pre-Cache This? │
├─────────────────────────────┬────────────────────────────┤
│ PRECACHE (install time) │ DYNAMIC (at runtime) │
├─────────────────────────────┼────────────────────────────┤
│ App shell HTML │ API responses │
│ Core CSS │ User-specific images │
│ JavaScript framework │ Lazy-loaded routes │
│ Logo and icons │ Third-party content │
│ Web fonts │ Search results │
│ Offline fallback page │ User-generated content │
│ Critical above-fold images │ Analytics scripts │
└─────────────────────────────┴────────────────────────────┘
Think of precaching like packing essentials for a camping trip. You pack a tent, sleeping bag, and food (pre-cache — things you definitely need). You do not pack firewood (you gather it there) or extra clothes for every weather (dynamic cache — only if needed).
Precaching with Workbox
// Workbox generates the precache manifest automatically
// In sw.js:
import { precacheAndRoute } from 'workbox-precaching';
// self.__WB_MANIFEST is injected during build
precacheAndRoute(self.__WB_MANIFEST);
Workbox generates a manifest like this during build:
// Generated manifest (example)
self.__WB_MANIFEST = [
{ url: '/index.html', revision: '3a8f1b2c' },
{ url: '/styles/main.abc123.css', revision: null },
{ url: '/scripts/main.def456.js', revision: null },
{ url: '/images/logo.svg', revision: '1e2d3c4b' },
{ url: '/fonts/inter.woff2', revision: null },
{ url: '/offline.html', revision: '5f6a7b8c' }
];
Manual Precaching
Without Workbox, you pre-cache manually:
const PRECACHE = 'precache-v1';
const PRECACHE_URLS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.svg',
'/fonts/inter.woff2',
'/offline.html'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE).then(cache => {
return cache.addAll(PRECACHE_URLS);
}).then(() => self.skipWaiting())
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(names => {
return Promise.all(
names.filter(name => name !== PRECACHE)
.map(name => caches.delete(name))
);
}).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
});
Precaching with Revisioning
Resources that change should include a revision in the precache manifest:
// Workbox handles revisioning:
// For versioned files (with hash in name), revision is null
// because the URL itself changes
// For unversioned files, revision is a content hash
// Manual revisioning:
const PRECACHE_MANIFEST = [
{ url: '/index.html', revision: '20260628-v2' }, // Manual revision
{ url: '/styles/main.abc123.css', revision: null }, // Versioned URL
{ url: '/api/config.json', revision: 'v3' } // Config file
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open('precache-v1').then(cache => {
return Promise.all(
PRECACHE_MANIFEST.map(entry => {
// Build cache key from URL and revision
const cacheKey = entry.revision
? `${entry.url}?rev=${entry.revision}`
: entry.url;
return fetch(entry.url).then(response => {
if (response.ok) {
return cache.put(cacheKey, response);
}
throw new Error(`Failed to fetch ${entry.url}`);
});
})
);
}).then(() => self.skipWaiting())
);
});
Precaching Large Assets
For large assets, consider progressive precaching:
// Progressive precaching
self.addEventListener('install', event => {
event.waitUntil(
caches.open('precache-v1').then(cache => {
// Cache critical assets first
return cache.addAll([
'/',
'/index.html',
'/styles/core.css',
'/scripts/main.js'
]);
}).then(() => {
console.log('Core assets cached');
// Defer non-critical precaching
return caches.open('precache-later-v1').then(cache => {
return cache.addAll([
'/images/hero-large.jpg',
'/fonts/roboto.woff2'
]);
});
}).then(() => self.skipWaiting())
);
});
Precaching with Priorities
// Priority-based precaching
const PRIORITY_LEVELS = {
critical: [
'/',
'/index.html',
'/styles/core.css',
'/scripts/main.js'
],
high: [
'/images/logo.svg',
'/fonts/primary.woff2',
'/offline.html'
],
medium: [
'/styles/themes/dark.css',
'/images/hero-small.jpg',
'/favicon.ico'
],
low: [
'/styles/print.css',
'/images/hero-large.jpg',
'/fonts/fallback.woff2'
]
};
self.addEventListener('install', event => {
event.waitUntil(
caches.open('precache-v1').then(cache => {
return cache.addAll(PRIORITY_LEVELS.critical);
}).then(() => {
console.log('Critical assets cached');
// Return to let activation proceed
return self.skipWaiting();
})
);
});
// Cache lower priority assets after activation
self.addEventListener('activate', event => {
event.waitUntil(
Promise.all([
// Clean old caches
caches.keys().then(names => {
return Promise.all(
names.filter(n => n !== 'precache-v1')
.map(n => caches.delete(n))
);
}),
// Cache high priority in background
caches.open('precache-v1').then(cache => {
return cache.addAll(PRIORITY_LEVELS.high);
})
]).then(() => self.clients.claim())
);
});
// Cache medium/low on first fetch
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
const allAssets = [
...PRIORITY_LEVELS.medium,
...PRIORITY_LEVELS.low
];
if (allAssets.includes(url.pathname)) {
event.respondWith(
fetch(event.request).then(response => {
const clone = response.clone();
caches.open('precache-v1').then(cache => {
cache.put(event.request, clone);
});
return response;
}).catch(() => caches.match(event.request))
);
}
});
Common Mistakes
- Pre-caching every page of the app. Users visit 2-3 pages on average. Pre-cache the app shell, not every possible route.
- Pre-caching large media files. Videos and high-resolution images bloat the install. Cache them dynamically on first view.
- Not updating the precache revision. When a file changes but its URL stays the same, the old version remains cached. Update the revision hash.
- Ignoring the offline fallback. Always include a fallback page in your precache so users see branded content instead of browser errors.
- Pre-caching more than 50 resources. Browser implementations of cache.addAll() may timeout with too many URLs. Batch large precaches.
Practice Questions
- What is the difference between precaching and dynamic caching?
- Why do versioned files (with hash in URL) not need revision tracking?
- How does Workbox generate the precache manifest?
- Why should you limit precache size, and what is a reasonable limit?
- How would you progressively pre-cache resources by priority?
Challenge: Create a precache manifest for a simple news PWA with 20 assets. Categorize them into critical (5), high (5), medium (5), and low (5). Implement progressive precaching where critical assets block install, high caches during activate, and medium/low cache on first fetch.
FAQ
Mini Project
Create a pre-cache Strategy for a photography portfolio PWA: critical (app shell, thumbnails CSS, main JS), high (logo, fonts, offline page), medium (category page templates), low (full-resolution images). Implement progressive precaching. Verify by measuring install time with DevTools and testing offline functionality.
What's Next
Precaching is set up. Now learn dynamic caching — caching responses as users interact with your app, ensuring new content becomes available offline.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro