Skip to content
Angular ng pwa 4 min read

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.

installModeBehavior
prefetchAll listed files are downloaded during SW installation.
lazyFiles are cached on first request, not before.

Glob patterns are matched against the build output, not your source tree. After a hashed build, /*.js matches files like /main-AB12CD.js automatically — 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.

OptionApplies toMeaning
strategybothfreshness (network-first) or performance (cache-first)
maxSizebothMax number of cached responses (LRU eviction)
maxAgebothHow long an entry is considered valid (30s, 5m, 1h, 7d)
timeoutfreshness onlyNetwork 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 against maxSize but 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 a prefetch asset group so the app always boots offline.
  • Use lazy install for heavy media and rarely-needed assets to keep the initial install fast.
  • Choose freshness for user-specific or volatile data and performance for stable reference data.
  • Always set a sensible timeout on freshness groups so slow networks fall back to cache instead of hanging.
  • Bound every data group with maxSize and maxAge to prevent unbounded cache growth and stale reads.
  • Order dataGroups from most specific URL patterns to least specific — the first matching group wins.
  • Rebuild and re-inspect ngsw.json after every config change; the runtime never reads ngsw-config.json directly.
Last updated June 14, 2026
Was this helpful?