Service Worker Caching
The Angular service worker is driven entirely by a declarative JSON file, ngsw-config.json. Instead of writing imperative fetch handlers, you describe what should be cached and how it should be served, and the Angular CLI compiles that into a runtime manifest. Getting these strategies right is the difference between a snappy, offline-capable PWA and one that serves stale data or never updates. This page covers asset groups, data groups, and the prefetch, lazy, freshness, and performance strategies that control them.
How ngsw-config.json is structured
When you run ng add @angular/pwa, a baseline ngsw-config.json is created at the project root. The two sections that matter most are assetGroups (for files that are part of your build — JS, CSS, images, fonts) and dataGroups (for runtime API responses). A minimal config looks like this:
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [],
"dataGroups": []
}
The $schema reference gives you editor autocompletion and validation. After editing the file you must rebuild (ng build) so the CLI regenerates ngsw.json, the actual manifest the service worker reads at runtime.
Asset groups: prefetch vs lazy
Asset groups control build-time files. Each group has an installMode and an updateMode, and these accept two values: prefetch (download everything immediately when the service worker installs) and lazy (cache files only after they’re first requested).
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": ["/assets/**", "/media/**", "/*.(svg|png|jpg|webp|woff2)"]
}
}
]
Use prefetch for the application shell — the files needed to boot the app offline. Use lazy for large or optional media that not every user needs, so you don’t bloat the initial install. A common pattern is installMode: "lazy" with updateMode: "prefetch": an asset is fetched lazily the first time, but once cached it eagerly updates on every new deployment.
installMode | Behavior |
|---|---|
prefetch | All listed files are downloaded during SW installation. |
lazy | Files are cached on first request, not before. |
Glob patterns are matched against the build output, not your source tree. After a hashed build,
/*.jsmatches files like/main-AB12CD.jsautomatically — never hard-code hashed filenames.
Data groups: freshness vs performance
Data groups apply to runtime requests that aren’t part of the build — typically your REST or GraphQL API. Each group’s cacheConfig.strategy is either freshness (network-first) or performance (cache-first).
"dataGroups": [
{
"name": "api-fresh",
"urls": ["/api/cart", "/api/notifications/**"],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 50,
"maxAge": "1h",
"timeout": "5s"
}
},
{
"name": "api-performance",
"urls": ["/api/products/**", "/api/categories"],
"cacheConfig": {
"strategy": "performance",
"maxSize": 200,
"maxAge": "12h"
}
}
]
With freshness, the service worker tries the network first and falls back to cache only if the request exceeds timeout. This is right for data that must be current — carts, account state, live feeds. With performance, the cache is consulted first and the network is hit only when the entry is missing or older than maxAge. This is ideal for slowly-changing reference data like product catalogs.
| Option | Applies to | Meaning |
|---|---|---|
strategy | both | freshness (network-first) or performance (cache-first) |
maxSize | both | Max number of cached responses (LRU eviction) |
maxAge | both | How long an entry is considered valid (30s, 5m, 1h, 7d) |
timeout | freshness only | Network wait before falling back to cache |
Only same-origin requests, or cross-origin requests you explicitly list in
urls, are intercepted. Opaque cross-origin responses count againstmaxSizebut their bodies can’t be inspected — keep third-party APIs out of data groups unless you control CORS.
Verifying the generated manifest
After building, inspect dist/<app>/ngsw.json to confirm your groups compiled as expected:
ng build
cat dist/my-app/browser/ngsw.json | head -n 20
Output:
{
"configVersion": 1,
"appData": {},
"index": "/index.html",
"assetGroups": [
{ "name": "app", "installMode": "prefetch", "updateMode": "prefetch", ... },
{ "name": "assets", "installMode": "lazy", "updateMode": "prefetch", ... }
],
"dataGroups": [ ... ]
}
In the browser DevTools Application -> Service Workers panel, a hard reload after deploy should show the worker activating and the Cache Storage entries (ngsw:/:...) populating according to your strategies.
Best Practices
- Keep the application shell (
index.html, CSS, entry JS) in aprefetchasset group so the app always boots offline. - Use
lazyinstall for heavy media and rarely-needed assets to keep the initial install fast. - Choose
freshnessfor user-specific or volatile data andperformancefor stable reference data. - Always set a sensible
timeoutonfreshnessgroups so slow networks fall back to cache instead of hanging. - Bound every data group with
maxSizeandmaxAgeto prevent unbounded cache growth and stale reads. - Order
dataGroupsfrom most specific URL patterns to least specific — the first matching group wins. - Rebuild and re-inspect
ngsw.jsonafter every config change; the runtime never readsngsw-config.jsondirectly.