← Back to results

Ship less, cache the rest

Case study · Swiss water-infrastructure GIS platform · Angular + Nx + nginx

In short: every visitor downloaded the whole app — including features they would never open — on every single visit. Now the main bundle is a third smaller, niche features load on demand, and a repeat visit downloads ~50× less.

Context

The same platform as the other case studies — an Angular/Nx monorepo served by nginx from a container. Years of growth had piled everything into one eager bundle, and the delivery pipeline had drifted: the "prod" build wasn't building on the production configuration, and the server told browsers to cache nothing.

Problem

Every user paid for everything. A specialist feature used by a narrow group shipped ~1.5 MB into everyone's main bundle; an audio library loaded eagerly for views most users never open; a date library pulled in ~100 locales when the app supports 12. And because every asset was served no-cache, returning users re-downloaded the entire app on every visit — fonts, icons and all.

Key changes

Change Before After Gain
Lazy-loaded a niche feature module ~1.5 MB raw / ~400 KB gzip of a specialist feature (a German drinking-water norm) shipped eagerly to every user own lazy chunk, loaded only when its route is visited — with its services moved into the lazy injector pages-main chunk −91%
Dynamic import for the audio library Howler statically imported — 108 KB raw in main for a player used on a few views import type + await import() at the use site — loaded on first play −108 KB raw / ~30 KB gzip
Locale tree-shaking import * as locales from 'date-fns/locale' dragged in ~100 locales of the whole world named imports for the 12 actually supported locales, resolved via a static map −150 KB raw / ~40 KB gzip
Dependency cleanup moment-timezone duplicating date-fns-tz, an unused 3D library, CJS lodash, devtools in prod deps duplicates and dead deps removed, lodash → lodash-es, build tools reclassified as devDependencies hundreds of KB out of the bundle
Production build that is actually production nx build without -c production — prod artifacts built on default configuration explicit production config for prod and stg, a dedicated stg target, consistent env naming tree-shaking & AOT actually on
Cache policy that uses the browser nginx served every static asset with no-cache — returning users re-downloaded everything, and a broken regex could put translations into long cache hashed assets: 1-year immutable; translations/config: explicit no-store; fixed regex, one clean Cache-Control per response, ETag revalidation back on repeat visit ~2.5 MB → ~50 KB
Precompressed responses nginx gzipped every response on the fly (level 6) gzipper precompresses at build time (level 9), nginx serves .gz via gzip_static lower CPU, faster TTFB, better ratio
webpack → esbuild application builder deprecated webpack-browser executor, webpack-dev-server Angular's application builder (esbuild) for both apps, Vite-based dev server, webpack-only hacks replaced with clean ESM imports prod build ~2 min → ~36 s

Measured (HAR, cold load)

Metric Before After Delta
main.js (raw) 7.18 MB 4.65 MB −35%
main.js (gzip) 1.70 MB 1.33 MB −22%
Cold-load transfer ~2.47 MB ~2.08 MB −16%
Repeat-visit transfer ~2.5 MB — no-cache re-downloaded everything ~50 KB — index.html + two JSONs −98%
Pages-main chunk 1.55 MB raw (eager niche feature inside) 0.14 MB raw −91%
Production build time ~2 min (webpack) ~36 s (esbuild) −70%

Bugs fixed along the way

Delivery work keeps surfacing latent bugs, and this round was no exception: SPA deep links 404'd on hard reload (missing try_files), the staging pipeline silently built the test target, a duplicated Cache-Control header left caching behavior up to whichever proxy interpreted it, and the lazy-loading migration exposed a service provided in the wrong injector — fixed by moving it into the lazy module where its dependencies live.

The pattern repeats

This is the second time the same playbook paid off — an earlier round at a US social-commerce unicorn cut that app's production bundle by ~40% with the same moves: lazy load what's niche, tree-shake what's imported too broadly, delete what's duplicated.

The fastest bytes are the ones you never ship — and the second-fastest are the ones you ship once. Lazy-load the niche, prune the duplicate, and let the browser cache do the job it was built for.

Related: a test suite from minutes to seconds