Frontend Performance/Shipping Less JavaScript
Lesson 19 of 20 · Episode 20

Compressing JavaScript

From source to wire: minification then gzip/brotli, and how much each step shaves off the transfer.

MinificationgzipbrotliTransfer size
Watch on YouTube ↗

JavaScript is the second-biggest thing most pages download — so shrinking the bytes on the wire is one of the highest-leverage wins. It happens in two stages: minification rewrites your code smaller, then HTTP compression (gzip / brotli) squeezes it further for the trip to the browser. This lesson covers both — and the trade-offs that can quietly cancel them out.

Stage 1

Minification

Minification makes the code itself smaller while keeping it valid JavaScript the engine runs identically. It strips comments and whitespace, drops dead code, and — the big one — mangles identifiers, renaming your firstNumbers to a. Step through it:

InteractiveThe same function, minified
Minification: source → wire-ready
// add two numbers together
function addNumbers(firstNumber, secondNumber) {
  const result = firstNumber + secondNumber;
  return result;
}
140 B
1/4 · Source
The code you write — comments, spaces, descriptive names. Readable, but heavy.
Comments → whitespace → mangled names. The JS engine runs all four identically; only the byte count changes.
Stage 2

HTTP compression

Minified code is still plain text full of repetition — perfect for a compression algorithm. Compression comes in two flavours: lossy (throws away detail you won't miss — great for JPEGs, ~50% smaller) and lossless (decompresses to a byte-perfect copy). Text must be lossless — a missing character would break your code — so HTML, CSS, and JS always use lossless algorithms like gzip and brotli.

Lossy is for media, lossless is for code
A JPEG can lose detail nobody notices. A semicolon can't. That's why images use lossy compression and text assets use lossless.
The two algorithms

gzip vs brotli — level vs speed

gzip is the ~30-year-old standard, supported literally everywhere. brotli (2015) builds on the same ideas (LZ77 duplicate-detection + Huffman coding) with a bigger window and a built-in dictionary, so it compresses tighter. Both expose a level: higher = smaller file but more CPU time, with diminishing returns at the top. Play:

InteractiveCompression ratio vs. compression speed
5
Original (minified)100.0 KB
After brotli -527.8 KB
Compression ratio
3.60×
Over the wire
27.8 KB
Compression speed
fast
Sweet spot. Mid-level brotli beats gzip's best ratio while keeping compression fast enough.
Crank the level and watch the file shrink — but the top levels barely help while costing real CPU. brotli's mid-range beats gzip's best.
When to compress

Static vs dynamic compression

Static (build-time) compression runs once when you build, saving a pre-compressed file on the server. Since it's offline, you can afford the highest levels — CPU cost doesn't touch request time. Best for assets that don't change often (your JS bundles). Dynamic (on-the-fly) compression runs per request in the server / proxy (nginx, Apache). Easier to set up and great for frequently-changing content — but you must use a lower level, or the compression time itself slows the response you were trying to speed up.

Static (build-time)Dynamic (on-the-fly)
WhenOnce, during buildPer request, at the server
Level you can affordHighest (offline)Lower (adds latency)
Best forStable bundlesFrequently-changing content
Done byBundler pluginnginx / Apache / proxy
How the browser knows

The browser/server handshake

You never decompress anything by hand. The browser advertises what it supports, the server picks one and labels the response, and the browser decompresses transparently before your code runs:

InteractiveAccept-Encoding → Content-Encoding
🖥
Browser → Server
Accept-Encoding: gzip, deflate, br, zstd
“Here's what I can decompress.”
🗄
Server → Browser
Content-Encoding: br
“I compressed it with brotli.”
The browser advertises what it supports; the server picks one and says which it used. The browser then does the decompression — transparently, before your JS ever runs.
The request says what the browser can handle; the response says what the server used. Decompression happens in the browser, automatically.

To check it's actually working: a Lighthouse audit flags “enable text compression” with the potential savings, and the DevTools Network tab shows the Content-Encoding header on each response.

The catch

Trade-offs with splitting & caching

Here's where the techniques in this module collide. Compression works better on bigger files — more repeated patterns to exploit. So aggressive bundle splitting into many tiny chunks can actuallyhurt the compression ratio. But a giant merged bundle hurts caching: change one line and the whole file's cache is invalidated, while split chunks let unchanged ones stay cached.

The three-way balance
Splitting (better caching + smaller initial load) vs. merging (better compression). The rule of thumb: split things that aren't related (separate routes), keep things that are related together. There's no universal answer — it depends on your app's change patterns and navigation.
Q1Multiple choice
What is “mangling” in minification?
Q2Multiple choice
Why must JavaScript use lossless (not lossy) compression?
Q3Multiple choice
Which headers negotiate compression between browser and server?
Q4Multiple choice
Why can over-aggressive bundle splitting hurt compression?
Key takeaways
  • Two stages: minification (smaller code) then HTTP compression (smaller transfer).
  • Minification strips comments/whitespace/dead code and mangles names — still valid JS.
  • Text uses lossless compression; brotli generally beats gzip at a given size.
  • Higher levels = smaller but slower; use high levels static (build), low levels dynamic (per-request).
  • Mind the three-way trade-off: splitting (caching) vs merging (compression).
← Previous
18. BundlePhobia
Next →
19. Virtual Scrolling