Performance
Published
“Obfuscation bloats your bundle” is one of those claims everybody repeats and nobody quantifies. So we quantified it. Below is the measurement across 42 real production libraries — jQuery, React DOM, three.js, Babel, D3, Chart.js and the rest — run through our own protection pipeline, with raw and gzipped sizes for every one.
The headline: raw size roughly doubles, gzipped size grows about a quarter. Those two numbers are far apart, and the gap is the whole story.
Method, so you can discount it appropriately
The corpus is the same 64-library set our engine uses as a release gate — real, shipped, mostly-minified library code rather than snippets. We measured the 42 files over 20 KB (smaller files are dominated by fixed overhead and tell you nothing useful).
- Settings: identifier renaming including globals, string extraction into a lookup table, string encoding. This is a deliberately aggressive configuration — it exercises as much of the pipeline as possible.
- Not included: self-compression, VM bytecode, or control-flow flattening. Those cost more. Numbers here are a floor for heavy modes, not a ceiling.
- Gzip is level 9. Brotli will differ somewhat; the direction is the same.
- Every output was checked to parse and to be free of dangling internals. These are sizes for working output, not for garbage.
The result
Split by whether the input was already minified, because the two groups behave nothing alike.
| Input |
n |
Raw, median |
Gzip, median |
Gzip, range |
| Already minified | 30 | 1.97× | 1.29× | 0.90× – 1.38× |
| Not minified | 12 | 0.95× | 0.99× | 0.49× – 1.38× |
Protection includes minification, so unminified input comes out roughly the size it went in: the shortening and the obfuscation overhead cancel. If you are protecting source that has never been through a minifier, your bundle does not grow at all, and sometimes it shrinks noticeably — acorn went from 54 KB gzipped to 38 KB.
If you are protecting an already-minified bundle, which is the common case, the honest number is 1.25× to 1.30× over the wire. Concretely:
| Library |
Raw before → after |
Gzip before → after |
Wire cost |
| jquery.min.js | 85 → 173 KB | 30 → 38 KB | +8 KB |
| react-dom.production.min.js | 129 → 260 KB | 42 → 53 KB | +11 KB |
| axios.min.js | 41 → 80 KB | 15 → 19 KB | +4 KB |
| three.min.js | 589 → 1379 KB | 146 → 190 KB | +44 KB |
| echarts.min.js | 1005 → 1868 KB | 326 → 410 KB | +84 KB |
| zod.min.js | 165 → 124 KB | 23 → 21 KB | −2 KB |
Why raw doubles but gzip barely moves
Because gzip is very good at exactly the thing obfuscation produces.
The two transforms that add the most bytes are identifier renaming (e becomes _0x1F4A2, seven characters where there was one) and string extraction (every literal moves into one array and each use becomes an indexed read). Both replace varied text with highly repetitive text. A file full of _0x1F4A2, _0x1F4B7, _0x1F4C1 compresses far better than a file full of genuinely different short names, and a string table is the most compressible structure in the output — it is one long run of similar-looking entries.
So the raw multiplier tells you about disk and parse work; the gzip multiplier tells you about the network. Only one of those is what your users wait for.
The outliers, in both directions
The spread matters more than the median if you are budgeting.
- Best case, 0.90×: zod, whose bundle is dominated by long unique strings that the string table deduplicates.
- Worst case, 1.38×: bootstrap.bundle.min.js and effector, both small relative to the fixed cost of the string table and both already very tightly minified — there was nothing left to squeeze.
- Largest raw multiplier, 2.51×: effector again, at 28 KB → 70 KB. Over the wire that is 12 KB → 16 KB. The scary-looking number and the number that matters are four kilobytes apart.
There is a pattern worth internalising: the raw multiplier is worst on small, dense files, and the gzip multiplier follows it only weakly. If you are protecting one small utility module, expect the raw file to look alarming and the transfer cost to be trivial.
What to actually do about it
- Measure gzipped, not raw. If your performance budget is written in raw bytes it is measuring the wrong thing for every asset, not just protected ones.
- Protect before compression, and let the server compress. Do not ship a pre-gzipped protected file and then let a CDN re-compress it.
- Protect selectively. Your licence check, pricing logic, and proprietary algorithms are worth the bytes. Your date formatter is not. Most build integrations let you scope protection to specific entry points, and a 30% increase on 15% of your code is a 4% increase.
- Re-measure if you enable heavier modes. Control-flow flattening and VM bytecode are a different order of cost. The numbers above do not cover them, and we would rather say so than let you find out in production.
The honest caveat
This is one engine, one configuration, one corpus, one compressor setting. Another tool with different defaults will land somewhere else, and the same tool with control-flow flattening turned on certainly will. What we would encourage is not to take our number but to run the same measurement on your bundle, which takes about five minutes: protect it, gzip both versions, compare. Any vendor unwilling to have that done to their output is telling you something.
And keep the size question separate from the strength question. A bigger output is not a better-protected one, and obfuscation raises the cost of reverse engineering rather than preventing it. Size is a budget line; protection strength is a different conversation.
Frequently asked questions
How much bigger does a protected bundle get?
It depends almost entirely on whether the input was already minified, and the two cases behave nothing alike. For already-minified input, which is the common case, the median was about twice the raw size and roughly 1.29 times the gzipped size, so the honest over-the-wire figure is about 1.25 to 1.30 times. For input that had never been minified, the median was about the same size it went in, because protection includes minification and the two effects cancel.
Why is the raw number so much worse than the compressed number?
Because the things protection adds compress extremely well. Renamed identifiers repeat, a string table is highly repetitive, and encoded literals share structure, so a compressor removes most of what the raw measurement counted. Raw size is the number people quote and the compressed number is the one users actually experience, which is why the gap between roughly two times and roughly a quarter more is the whole story.
Can a protected bundle end up smaller than the original?
Yes, when the input was never minified. Protection performs minification as part of its work, so unminified source can come out noticeably smaller: one library in the measurement dropped from 54 KB gzipped to 38 KB. If you are protecting source that has not been through a minifier, bundle growth is not a reason to hesitate.
Do these numbers cover the heaviest protection settings?
No, and that is stated deliberately. The measurement used identifier renaming including globals, string extraction into a table, and string encoding, but excluded self-compression, virtual machine bytecode and control-flow flattening, all of which cost more. Read the figures as a floor for heavy modes rather than as a ceiling. Compression level was set to nine, and a different compressor will shift the exact numbers while leaving the direction unchanged.
How was the corpus chosen, and why does it matter?
It is the same library set the engine uses as a release gate: real, shipped production code rather than snippets, measured across the files above 20 KB because smaller files are dominated by fixed overhead and tell you nothing useful. Every output was also checked to parse and to be free of dangling internals, so the sizes describe working output rather than broken output that happened to be small.
Related reading