Engineering
Published
Short answer: bundle, minify, then protect — protection is the last thing that touches your JavaScript before it becomes an artifact. It is worth understanding why, because the two wrong orders fail in different ways, and one of them fails silently enough that teams ship it for months without noticing.
The correct order, and what each step needs from the previous one
your source
-> transpile (TypeScript, Babel)
-> bundle (Vite, webpack, Rollup, esbuild)
-> minify (terser, esbuild — usually part of the bundler)
-> PROTECT (obfuscation, the last step)
-> ship the artifact
Each stage in that list needs to understand your code. A bundler resolves imports, a tree shaker decides what is unreachable, a minifier proves a variable is never read. All of that is static analysis, and static analysis needs code that looks like code.
Protection's entire purpose is to make code stop looking like that. So it has to come after every tool that reasons about your program — not because of a technical prohibition, but because those tools do their jobs by understanding structure, and protection is the deliberate destruction of structure.
Wrong order 1: protecting before you bundle
This is the one that looks reasonable. You protect each source file, then hand the protected files to your bundler and let it do its normal work. It fails in four ways, and none of them produce an error message.
- Tree shaking stops working. A tree shaker decides a function is unused by tracing references. Once
utils.formatDate is a computed lookup like u[a[17]], that trace fails — the bundler cannot prove the property is unread, so it keeps everything. Your bundle grows, sometimes dramatically, and the cause is not obvious from the output.
- Side-effect analysis breaks the other way. Bundlers use
/*#__PURE__*/ annotations and the sideEffects field to drop code that provably does nothing. Protection rewrites the expressions those annotations were attached to. Best case you lose the optimisation; worst case a bundler concludes a module is side-effect-free when it is not.
- The minifier undoes work and adds nothing. Running a minifier over already-protected code means renaming generated names to other generated names. You gain almost no bytes, you spend build time, and you introduce a second tool's opinions about your control flow.
- You protect the same code many times. Your dependencies get protected once per build, which is slow, and shared vendor chunks stop being shared because each copy is protected independently.
The tell is a bundle that is much larger after you introduce protection than the size increase from protection alone would explain. If protection roughly doubled your raw size, that is expected. If it grew fivefold, you have probably disabled tree shaking.
Wrong order 2: minifying after you protect
Less common, more destructive. A minifier is an optimiser, and several of protection's outputs look to it exactly like code worth optimising away:
- Dead-code injection gets removed. Unreachable branches added to confuse a reader are, to a minifier, dead code with a well-known name. It deletes them. You paid the size cost during protection and then paid again to have the benefit stripped.
- Control-flow flattening gets partially unwound. Constant folding and branch simplification are precisely the techniques used to reverse dispatch-based flattening. A minifier applies them automatically and enthusiastically.
- Property mangling collides. If both tools rename properties, the second one renames the first one's output without knowing which names were deliberate. This is the arrangement most likely to produce a build that is broken rather than merely weaker.
- String tables get inlined. An aggressive minifier may decide a single-use table entry should be folded back to its literal, putting your strings back where they started.
The net result is a build that is slower, bigger, and less protected than either tool alone would give you. Worse, it looks fine — the output is obfuscated to the eye, so nobody checks whether the transforms they paid for actually survived.
"Should I minify at all, then?"
Yes. Minification and obfuscation solve different problems and the pipeline wants both — the distinction is covered properly in minification vs obfuscation. Minify as part of your bundler, where it belongs, and let protection run on the minified bundle.
One nuance: because your minifier has already shortened local names, protection's own renaming adds less on top than you might expect. That is fine. Renaming is the cheapest thing protection does and not the reason you are running it — the value is in the string, structure, and member transforms, none of which a minifier performs.
Practical wiring
- Run it as a post-build step over your output directory, not as a loader or transform inside the bundler. A loader runs per-module, before bundling — which is wrong order 1.
- Protect emitted assets by name. Make sure your patterns catch every entry point, including workers and any lazily loaded chunks. A glob that matches
main.*.js and misses chunk-*.js ships most of your app unprotected.
- Generate source maps before protection and do not ship them. Keep them for your error tracker. A map next to a protected bundle undoes the entire exercise — see your source maps are publishing your source code.
- Keep the identifier map from each protected build so production traces can be demangled. That is what symbolication uses, and the workflow is in debug obfuscated JavaScript in production.
- Use a seed for reproducible artifacts. The same input, options, and seed produce byte-identical output, which is what you need for build attestation and for comparing two releases.
- Verify after, every time. Run your test suite against the protected artifact, not the pre-protection bundle. Pipeline wiring is exactly the kind of thing that works for a year and then quietly stops when someone adds a chunk.
Concrete configuration for the common bundlers and CI systems is in build integration and release workflows.
The short version
Protection goes last. Everything that needs to understand your code runs first; the step whose job is to make your code hard to understand runs at the end, and nothing runs after it except the upload. If you remember only one thing: a tool that optimises, bundles, or analyses should never see protected output.
Related reading