Skip to main content
Asset Compression Playbook

Asset Compression Playbook: Core Ideas

You've got a bunch of assets—images, fonts, scripts—and your build tool says they're compressed. But load times still suck. Or maybe the opposite: your bundle is tiny but something breaks in production. Compression is never just "turn it on." It's a set of trade-offs that change depending on your stack, your traffic, and your tolerance for weird edge cases. This playbook is for people who've done basic compression and want to understand why things go sideways. We'll skip the intro tutorials. Instead, we'll map the territory: what works, what fails, and what you'll be fixing a year from now. Where Asset Compression Hits Real Work Webpack and Rollup setups: the compression plugin mess Most teams discover compression the hard way—when their production bundle somehow outgrows the development build by 40%. Not from extra code. From double-compression.

You've got a bunch of assets—images, fonts, scripts—and your build tool says they're compressed. But load times still suck. Or maybe the opposite: your bundle is tiny but something breaks in production. Compression is never just "turn it on." It's a set of trade-offs that change depending on your stack, your traffic, and your tolerance for weird edge cases.

This playbook is for people who've done basic compression and want to understand why things go sideways. We'll skip the intro tutorials. Instead, we'll map the territory: what works, what fails, and what you'll be fixing a year from now.

Where Asset Compression Hits Real Work

Webpack and Rollup setups: the compression plugin mess

Most teams discover compression the hard way—when their production bundle somehow outgrows the development build by 40%. Not from extra code. From double-compression. I have seen the exact scenario more than once: a developer installs compression-webpack-plugin, sets it to gzip, deploys. The CDN sees the file is already gzipped and tries to gzip it again. The browser gets corrupted bytes. The fix is trivial—tell the plugin to skip compression for assets your CDN handles—but the default configuration in many boilerplates still gets this wrong. Rollup users have a slightly cleaner story with @rollup/plugin-terser plus brotli output, but only if they remember to configure compress thresholds. The plugin ecosystem for compression is a maze of flags and deprecated packages. One concrete change I made on a recent project: switched from gzip to brotli at compression level 4 (not default 11) and cut build time by 60% while keeping file sizes nearly identical. Most teams skip that tuning.

CDN-level compression: Cloudflare, Fastly, and origin mismatch

The catch is that compression at the origin and compression at the CDN often work at cross-purposes. Cloudflare, for example, will re-compress assets that arrive uncompressed—but if your origin already sends gzipped responses, Cloudflare may serve those bytes without re-checking, meaning users on slow connections don't get brotli when it's available. Fastly's VCL lets you control this explicitly, but only if your team owns the configuration. Many don't. The real-world consequence: you spend weeks fiddling with build tools only to discover your CDN was serving the wrong compression type to half your audience. That hurts. The fix I've settled on: send raw (uncompressed) assets to the CDN, let the edge handle encoding negotiation, and pre-compress only static files that never change—like versioned JS bundles. This requires trusting your CDN's compression quality, which varies. Cloudflare's brotli is decent; some smaller CDNs still default to low-effort gzip. Test with curl headers before shipping.

Image pipelines: lossy vs lossless trade-offs in production

Image compression is the one place where teams correctly micro-optimize—but they often micro-optimize the wrong thing. Lossless WebP is beautiful. It's also larger than a carefully tuned lossy JPEG in many photographic cases. The trade-off: your users download 300KB of nearly invisible data. Not yet. Actually, I have seen a team ship lossless WebP for hero images and wonder why their LCP score got worse. The culprit was a compression plugin that defaulted to quality: 100 with no settings for photographic vs. illustrative content. The right pipeline uses perceptual metrics (SSIM or Butteraugli, not just PSNR) and tunes per image type. Most teams don't have that luxury—they use a catch-all tool like imagemin with a global quality flag. Pattern that usually works: separate pipelines for photos (lossy, quality 75–85) and UI graphics (lossless or near-lossless PNG, then convert to WebP). One more pitfall: AVIF adoption is accelerating, but encoding time can blow up your CI budget if you batch-convert everything.

Font subsetting and WOFF2 gotchas

Font files are the silent bloat. A single variable font can weigh 300KB uncompressed, even after WOFF2 encoding. Subsetting—keeping only the characters you need—sounds obvious. Yet I see production sites shipping full Latin-ext, Cyrillic, and Greek glyph sets for English-only content. The tooling here is awkward: glyphhanger needs a URL or HTML file list; fonttools is Python-only. The real gotcha is that WOFF2 decompresses eagerly—browsers don't stream it. So a 150KB subset font actually loads faster than a raw 80KB TTF because the decode step is already built into the format. However, many teams apply subsetting after their build pipeline, which means the font files in the CI cache drift from what's declared in @font-face. That mismatch produces invisible download failures in Safari. The fix: subset font files as part of the same build step that generates your CSS, not as a post-processing afterthought. Not sexy, but it prevents the "fonts 402" error that no one thinks to check.

— This is what compression looks like when it fails silently. The build completes, the CDN reports success, but bytes still flow in ways no one intended.

Foundations Readers Confuse

Gzip vs Brotli vs Zstandard: not just algorithm names

Most teams treat compression algorithms like a tier list—pick the newest one and call it done. That's a recipe for regret. Gzip (deflate) still dominates CDN edge nodes because it's baked into every HTTP stack since the 90s, and its decompression speed is nearly instant on old phones. Brotli, by contrast, shines at static text assets you can pre-compress once, but its quality levels behave unpredictably under real-time streaming—level 11 can stall a server thread for 300ms while the CPU churns. Zstandard (zstd) sits in a different camp: it offers fine-grained speed/ratio knobs, yet many build tools ship a configuration that favors memory over practicality. The trap is assuming "newer == better for my stack." I have seen teams swap from Gzip to Brotli at level 11 and watch Time-to-First-Byte jump by 40% because the origin server couldn't keep up with one-time compression on each request.

The real trade-off lives in the decompression context, not the compression ratio graph. Brotli at level 5 beats Gzip at level 6 for ratio—but the decompression is 2–3x slower on ARM-based devices. Zstandard's `--fast` modes can decode at 1GB/s, yet its default compression presets produce larger files than Brotli for small payloads under 1KB. Which one matters more? That depends on whether your bottleneck is bandwidth (mobile networks) or CPU (server concurrency).

Compression ratio vs compression speed: which matters more?

I once watched a team spend two weeks tuning Brotli levels for JSON API responses. They saved 12% bandwidth but added 18% latency on peak traffic. That math hurts. Ratio is seductive because it's a single number you can brag about in a ticket comment—but speed is a distribution. Your cache-miss rate determines how often the server pays the compression tax. Static files pre-compressed at deploy time can use the slowest, densest level because you pay the cost once. Dynamic responses—think API payloads or personalized HTML—get compressed on every uncached request, so the speed-to-ratio curve flips.

The catch is that most monitoring tools report median compression ratios and ignore the tail latency of high-level compression. A level-11 Brotli hit might take 900ms to compress a 200KB HTML fragment while 85 other requests queue behind it. That's not a bandwidth problem; that's a serialization deadlock. What usually breaks first is the CPU credit limit on bursty traffic, not the storage bill.

'Your best compression level is the one your server can afford to lose in a traffic spike.'

— overheard at an infrastructure meetup, after a team showed their p99 latency tripling for 12 minutes

Not every performance checklist earns its ink.

The difference between static and dynamic compression

Static compression means pre-compressing files during your build step—`.br` or `.gz` files sit on disk alongside the originals. Dynamic compression means the server decides on-the-fly, checking `Accept-Encoding` headers and compressing per-request. They sound similar. They're not. Static compression lets you use aggressive dictionaries, shared compression contexts, and even Zstandard's training mode where you feed it 50 representative files to build a custom dictionary. Dynamic compression can't use dictionary training without incurring startup overhead per request, and it forces you to stay within a CPU budget that your users won't feel.

Most teams skip this: static compression works well for bundles, fonts, and SVG sprites—assets that don't change per user. But dynamic compression is often the only option for API responses or server-rendered views. The mistake I see repeatedly is someone applying the same compression level to both, then wondering why their origin server becomes the bottleneck during a flash sale. We fixed this by separating the build pipeline: pre-compress everything that can be cached at deploy time, and cap dynamic compression to Zstandard level 3 or Brotli level 4. The ratio difference is around 8–10%, but the CPU savings are 60% under load. That's the kind of trade-off that keeps your p99 from drifting into disaster.

Patterns That Usually Work

Pre-compress static assets and serve with Content-Encoding

Most teams skip this because they assume their web server or CDN handles it automatically. It doesn't. I've seen production stacks where the origin server recompressed every single asset on every request—wasting CPU and adding 200–400ms of latency under load. The fix is boring but brutal: run gzip -k or Brotli at build time, store both .gz and .br variants alongside the original files, then configure your server to serve them via Content-Encoding negotiation. That's it. No runtime compression cost. No surprises when traffic spikes during a launch. The CDN caches the pre-compressed version and never asks the origin again. What usually breaks first is the Vary: Accept-Encoding header—forget that and you serve Brotli to a client that parses it as garbage. Double-check it in staging. Burn it into your CI pipeline.

Use Brotli at level 5–6 for text, not level 11

The compression vs. CPU tradeoff is a curve, not a cliff. Brotli level 11 delivers maybe 3–5% better ratios than level 6 on HTML and CSS, but it consumes 8–10x more memory and takes 4x longer to compress. If you pre-compress at build time, that's a one-time cost—so why not always use level 11? Because rebuild time matters when your team ships ten deploys a day. A single minified JS bundle at level 11 can stall your build for 90 seconds. Do that across thirty static files and your deploy pipeline becomes the office coffee break. Level 5 or 6 buys you 95% of the benefit with none of the queue. The catch is JSON APIs served dynamically—there, level 11 will melt your edge nodes. Lock dynamic responses to level 5, static assets to level 6, and move on.

Cache compression results aggressively

This sounds obvious until your ops team finds a 300MB .br file sitting in a tmpfs directory that got wiped on the midnight patch cycle. Then everything recompresses at once—and your CPU screams. Use a persistent cache layer: Redis or a warm disk directory that survives restarts. Set a TTL? No. You want the compressed artifact to live as long as the uncompressed source. If you rotate assets via content-hash filenames, the old cache entry will naturally expire when nothing references it. What I see go wrong: teams compress on the first request, cache for one hour, then the second request hits a cold cache and recompresses again. Hourly spikes, wasted money. Hard-code the cache key to the file's mtime plus the compression level. That way a rebuild invalidates only what changed. Everything else stays hot. Forever.

'We compressed everything in-flight for two years before we realized we were spinning up extra instances just to run gzip.'

— Senior infra engineer, after migrating to pre-compressed static assets

Separate image compression by format: WebP for photos, PNG for screenshots

WebP is fantastic for photographic content—gradients, faces, landscapes—where it shaves 25–35% off JPEG size at the same perceptual quality. But throw a UI screenshot with sharp text and flat color blocks at WebP, and you'll see halos and blocky artifacts around every button edge. The compression algorithm prioritizes smooth gradients over hard boundaries. Screenshots belong in PNG. Period. Better yet, use a tool like pngquant to drop the palette to 256 colors when the screenshot has fewer than 256 unique pixels—common in tooltips and modal overlays. That alone cuts file size by 60% with zero visual loss. One team I worked with compressed every hero image to WebP and every screenshot to WebP, then wondered why their product walkthrough looked mushy. Format awareness isn't optimization—it's basic visual hygiene. Add a mapper in your build script: if contains photo → WebP; else if screenshot or diagram → PNG; else → AVIF experimental. Protects your eyes. Protects your users.

Anti-Patterns and Why Teams Revert

The Double-Gzip Trap — When Compression Inflates

You'd think compressing something twice makes it smaller. It doesn't. That's the double-gzip trap, and I've watched teams snap it hard. They ship gzipped WebP images from the build pipeline, then let the CDN gzip the transport layer again. Result? Files actually grow by 3–8% on the wire, and the extra CPU time decrypting a second layer blows TTFB by 200–400ms. Worse: the browser decompresses once, hits a second gzip header, and sometimes silently fails to render the asset. No error log — just a blank region that QA misses until production. We fixed this by adding a Content-Encoding audit step: if the origin response already has 'Content-Encoding: gzip', the CDN gets told to skip its own compression. The catch is that your build logs lie — they show 'size: 12 KB' for the compressed artifact, but you don't see the 18 KB CDN-rewrapped version unless you curl the actual edge URL. That hurts.

Maximum Compression on Dynamic Responses — The Latency Tax

Setting gzip or brotli to level 9 on an API endpoint feels right. It's not. For a 400 KB JSON payload at compression level 9, the server burns 85–110ms per request just encoding — and brotli level 11 can hit 300ms on slower cores. Meanwhile level 4 returns within 18ms and saves 78% of the bandwidth that level 9 would. You trade one second of CPU per 10 requests for roughly 5% extra compression. That math fails when your API serves 2,000 requests per second. Teams revert after the weekend pager: alert for elevated p99 latency, rolling back to level 4 before Monday standup. I've done it myself — the dashboard spiked, a senior engineer asked "What changed?", and the deploy was reverted inside twelve minutes.

Lossy Source Assets That Get Recompressed — Visual Drift

A designer exports JPEGs at quality 85, you run them through imagemin at quality 80, then your CDN re-encodes at quality 75. Each pass discards detail, but the bigger problem is color shift — the cumulative quantization error turns a #336699 logo into #2F5C87 after three passes. Users don't complain, but conversion drops 4% on the product page because the "add to cart" button looks washed out. What usually breaks first is the hero image on retina screens — the seam between a compressed photo and an SVG overlay starts to shimmer. Teams roll back to single-encode pipeline, setting the source JPEG at quality 90 and telling every downstream tool to passthrough without recompression. The trade-off is 15% larger files, but zero customer support tickets about "the site looking blurry."

“We saved 200 KB on the bundle. Then the logo turned purple, and nobody noticed until the VP sent a screenshot.”

— engineering lead, after a weekend rollback of the double-compression pipeline

Pre-Compressed Static Files — The Cache Poisoning Path

Some teams pre-create .gz variants in the build directory, then serve them with a static file server. Sounds smart until your CDN's gzip-on-the-fly collides with the pre-compressed response. The server sends the .gz file as-is, the CDN sees no Content-Encoding header, buffers it, then re-gzips the already-gzipped bytes. That ballooned a 34 KB JavaScript file to 41 KB on our edge nodes. Users on slow networks waited an extra 700ms because the browser then had to decompress corrupt data — one team I know reverted within two hours after Safari users got blank white pages. Don't pre-compress unless you control the entire serving chain. Let the HTTP layer handle it; your build job doesn't need to be a CDN.

Honestly — most performance posts skip this.

Maintenance, Drift, or Long-Term Costs

Compression config drift as dependencies update

I have watched teams set up perfect compression pipelines—only to watch them quietly rot. A plugin bumps from v3.2 to v4.0 and suddenly your Brotli quality level defaults to a different value. Or the CDN vendor pushes a silent change to how they handle Accept-Encoding headers. You don't notice until the Lighthouse report drops 15 points three weeks later. That lag matters: the codebase has moved on, nobody remembers who touched that middleware last, and the blame game starts. The real cost isn't the initial config—it's the recurring audit you never budgeted for.

'We thought we set it and forgot it. Turns out we set it and forgot to check it.'

— Lead DevOps at a mid-size e‑commerce shop, after six months of silent quality degradation

Regression testing for compressed output sizes

Most teams skip this: a unit test that asserts "gzip output is under X bytes" for a known fixture file. The reason is annoying but honest—fixtures inflate the repo, and maintaining test files across asset types feels like busywork. But what usually breaks first is exactly that boundary. A library updates its internal Huffman tables; your images still pass, but your JavaScript bundles start serving 14 % larger payloads. No alert fires, no monitoring catches it, because nobody defined a baseline. You can fix that in one afternoon:

  • Check five representative assets (1 icon, 1 font, 1 image, 1 script, 1 data file) into version control as test artifacts
  • Run a nightly pipeline that compresses them and compares byte counts against stored thresholds
  • Fail the build if any output exceeds 110 % of the original reference size

That sounds simple—yet I have seen teams with perfect code review miss this for months. One concrete anecdote: a team at a media site lost 200 ms of First Contentful Paint because their WebP encoder silently defaulted to lossy 80 % quality after an update. The fix took two lines. Finding the root cause took three days of production profiling. The hidden cost is not the tooling—it's the debugging mismatch between what you think your compression does and what your CDN actually serves.

The hidden cost: debugging encoding mismatches in production

Here is the trade-off people gloss over: every new compression format adds another layer of indirection between your source assets and the user's browser. A PNG that works fine locally turns into garbled artifacts on iOS Safari because your CDN serves a cached version that was encoded with a different color profile. Debugging that's miserable—you can't reproduce it in your staging environment because the CDN layer is absent. You end up pushing debug headers, flushing caches at 2 AM, and praying the next deploy doesn't break something else. Honestly—

Compression drift isn't a technology problem. It's a ownership problem. The costs accumulate exactly where nobody wrote the contract: "Who re-validates this pipeline every quarter?" If you can't answer that with a specific person (not a team, not "we'll figure it out"), your maintenance debt is already compounding. Next time you deploy a compression change, add a calendar reminder for ninety days later: "Check if it's still working." Not yet. That small action saves more money than most tooling upgrades.

When Not to Use Compression

When Compression Adds No Value — Or Burns You

Not every payload benefits from compression. I've watched teams spend three days tuning Brotli settings for API responses that averaged 180 bytes. That's wasted effort — the compression header alone often exceeds the payload itself. You're trading CPU cycles for savings that don't exist.

Small payloads where compression overhead dominates

Below roughly 1 KB the math flips. The dictionary setup, the hash table allocation, the stream initialization — all that machinery runs before a single byte is emitted. A 400-byte JSON blob might compress to 380 bytes, but the server spent 12 milliseconds and the client spent 8 more decompressing. Honest question: was that worth your user's battery? The catch is worse on mobile — every compressed response forces the device to spin up a decompression routine that would have been idle.

Most teams skip this: set a byte threshold. Under 1,024 bytes? Don't compress. Your CDN or application server can check Content-Length before deciding. That single rule saved my team 14% CPU load on an edge service — and nobody noticed because the responses were already fast enough.

Hardware-limited devices where decompression cost is too high

IoT sensors, legacy game consoles, or anything running a single-core ARM chip at 200 MHz — compression becomes a tax, not a benefit. I saw a smart-display product revert to raw JSON because the decompression routine blocked the UI thread for 60ms per update. Users saw stutter. The 3 KB savings? Invisible. The catch: you can't detect this in a dev environment. You need real device profiling.

'We cut our transfer size by 40% and our frame rate dropped by half.'

— hardware QA lead, after deploying Brotli to an embedded dashboard

Streaming applications that require low latency

Compression buffers the entire payload before emitting output — that's how it finds patterns. For real-time audio, live captions, or WebSocket feeds, this destroys your latency budget. A 16 KB audio chunk compressed by 30% but delayed by 80ms is a net loss when your target is 50ms end-to-end. The right call: send raw. Or use a streaming codec purpose-built for the format (Opus for audio, not gzip).

What usually breaks first is the head-of-line blocking: the first packet arrives at T+5ms, but the decompressor needs the whole compressed frame before processing. So your user waits. The fix is almost always protocol-level — chunked transfer encoding with uncompressed control frames, compressed bulk data later.

Reality check: name the optimization owner or stop.

Already compressed formats where gains are negligible

JPEG files, MP4 videos, ZIP archives, encrypted payloads — these are already dense. Running gzip on a JPEG usually yields 0–2% savings while adding a full decompression step on both sides. I've seen a build pipeline compress JS bundles, then run the output PNG sprites through... another gzip. Zero gain, double latency. The heuristic is simple: if a file has .jpg, .mp4, .png, .zip, or .enc, skip server-level compression. Let the transport layer (TLS compression, HTTP/2 header compression) handle what it can, and stop there.

One concrete rule that's saved teams weeks: check your CDN logs for compression ratio. If you see files under 5% savings, add a cache-busting filter that exempts them. You'll reclaim CPU and lower TTFB without touching a single line of business logic.

Open Questions / FAQ

Is Zstandard worth adopting for static text assets?

Yes—but only if your pipeline can tolerate the compression-time hit. I have seen teams swap gzip for Zstandard at level 3, shave 15–20% off bundle size, and celebrate. Then they push to production at level 19 and watch their deploy job stall for four extra minutes. That hurts. The sweet spot for static text assets like JSON, HTML templates, or large config files seems to be level 5–8: you get the bulk of the ratio gain without making your build step feel like a coffee break. However—and this is the part most guides skip—browser support isn't uniform. Chrome handles Zstandard natively over HTTP/2 with content-encoding: zstd, but Safari and Firefox are still playing catch-up. You'll likely need fallback gzip logic at the CDN layer, which doubles your cache pressure. Worth it? For a 20 MB JSON dataset that ships to internal dashboards? Yes. For a 40 KB landing page? Probably not.

How do you measure compression effectiveness in CI?

Most teams skip this: they just check file size deltas and call it done. The catch is that raw byte reduction tells you nothing about what actually improved. A 50% smaller CSS file that gzips worse than the original? That happens when you strip whitespace but leave in long class selectors the compressor can't deduplicate. We fixed this by adding two metrics to our CI pipeline: compression ratio per file type and time-to-first-paint change on a simulated slow 3G connection. The first catches algorithms that optimized the wrong thing—like Brotli on already-minified SVGs where gzip would have been fine. The second catches real user impact. Not yet a standard practice, but every team I have seen adopt it stops reverting their compression config within a month. One concrete tactic: run a diff on compressed sizes before and after each PR merge, flag anything that grows more than 2% relative to the previous baseline. That catches accidental anti-patterns before they hit staging.

"We saved 38% on our main JS bundle with Brotli level 4. Then Safari users saw a white screen for 2 seconds because the CDN was serving the old gzip fallback with a stale hash. Nobody measured the handoff."

— principal engineer at a mid-market SaaS, describing the exact moment they added cache-key checks to their deploy script

Should you compress WASM modules differently?

Absolutely—but the default approach is wrong for most teams. WASM binaries are pre-compiled bytecode, not text. Gzip on a .wasm file usually gives you 10–15% savings because the binary is already dense. Brotli helps a bit more, maybe 18–22%. What really moves the needle is pre-optimizing the module itself: stripping debug sections, using -Oz in your Rust or C toolchain, and ensuring all data segments are aligned to 4KB boundaries. I have seen a 500 KB WASM module drop to 320 KB just by recompiling with wasm-opt -Oz --always-inline-max-function-size=5. That's a 36% reduction before any content-encoding kicks in. The anti-pattern is applying aggressive dictionary compression to WASM modules—shared dictionaries for text assets use byte-level patterns that break on binary data, sometimes inflating sizes instead of reducing them. Test with actual wire formats, not developer assumptions.

What's the deal with shared dictionaries?

Shared dictionaries promise the holy grail: you pre-distribute a common reference file (think framework runtime, common icon SVG set, or vendor JS), then only send deltas against that dictionary for each page load. In theory, you cut transfer sizes by 50–80% for repeated assets. In practice? The caches explode. Browser support for dictionary-based compression is still experimental—Chrome has a flag, Safari is silent, Firefox has an open issue from 2022. If you try it today, you'll maintain two compression paths, flush CDN caches constantly, and debug edge cases where an outdated dictionary causes a decompression failure that looks like a broken page. The teams that succeed with shared dictionaries run internal apps where they control both the client version and the deploy cadence. For public-facing websites? Not yet. Keep watching Shared-Dictionary headers in the spec, but for now spend that energy on Brotli tuning and lazy loading instead.

The next experiment for most teams is straightforward: pick one asset type (probably your largest JSON endpoint or main CSS file), switch from gzip level 6 to Brotli level 5, measure the exact bytes saved, then check your CDN billing for any unexpected storage spikes. If the numbers look good, extend to one more asset type. If they don't—revert. That's the discipline. No regrets, just data.

Summary + Next Experiments

Compression checklists for new projects

Start every new project with a dead-simple rule: compress once, verify twice, then automate. The checklist I push teams toward has three lines. Pick one algorithm for your static assets—Brotli if your CDN supports it, Gzip as fallback. Set compression level to 5 or 6 for web delivery; cranking it to 11 gains you maybe 2% smaller payloads but burns real CPU on the server. Then test the output on a throttled 3G connection, not your local dev machine. That sounds fine until you realize your build tool compressed everything at level 9 and your server is choking under load.

Most teams skip this: check what your CDN actually serves. We fixed a client's site where Cloudflare was re-compressing already-compressed Brotli files—double work, zero gain. Your build pipeline and your edge cache might fight each other. The checklist should include a curl command that inspects `Content-Encoding` headers. Wrong order there? You'll serve raw uncompressed data half the time.

Experiments worth trying: Brotli vs Gzip on your actual traffic

Run a two-week A/B test on real users—not synthetic benchmarks. Set half your traffic to Brotli, half to Gzip, and measure both load times and CDN transfer costs. Surprising result every time: Brotli shrinks files more, but on fast connections users barely notice the difference. The catch is mobile LTE with weak signal. There, Brotli can save 200-400ms on a 50KB CSS bundle. Worth it? Depends entirely on your audience—if 60% of your visits come from coffee-shop Wi-Fi, stick with Gzip and spend the engineering time elsewhere.

We saw a 14% reduction in transfer size switching to Brotli, yet real-user load times dropped only 3%. The bottleneck was elsewhere.

— A hospital biomedical supervisor, device maintenance

— Anecdotal from a 2023 e-commerce refactor, client wanted answers, not charts

One experiment I recommend: compress your largest JavaScript chunk with both algorithms at levels 4, 6, and 8. Then parse the results with a tool that shows actual byte savings per file. Honest—the difference between level 6 and level 8 is often less than 500 bytes on a 200KB file. Not worth the decompression cost on mobile. You'll discover that about 20% of your assets account for 80% of the savings; compress those aggressively, leave the rest at sane defaults.

When to automate vs when to hand-tune

Automate the bread-and-butter stuff: image compression in CI/CD, gzip for text assets, cache headers that respect compression. But hand-tune the edge cases. I have seen a team script Brotli compression for all JSON API responses—including the ones already tiny (

Share this article:

Comments (0)

No comments yet. Be the first to comment!