Engineering

Minify before or after obfuscation? Getting the build order right

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.

Frequently asked questions

Should I minify before or after obfuscation?

Before. The correct order is bundle, then minify, then protect. Each step needs the previous one to have finished: the bundler needs real module structure to resolve and tree-shake, the minifier needs readable code to reason about safely, and protection needs a single finished artifact to transform. Protection goes last because anything that runs after it has to re-analyse code that was deliberately made hard to analyse.

What breaks if I protect before bundling?

Tree shaking and module resolution, mostly. A bundler decides what to include by following imports and exports and by proving that unused code is unreachable, and protected input frustrates both. You end up with a larger bundle that includes code you do not ship, and in some configurations with resolution failures where the bundler can no longer follow a reference it expected to understand.

What breaks if I minify after protecting?

Two things, and one of them is silent. The minifier will try to simplify constructs that protection added deliberately, undoing part of what you paid for. More seriously, minifiers apply assumptions about code shape that protected output can violate, so the result is occasionally a build that parses and passes a smoke test while behaving differently in an edge case. Running an optimiser over deliberately unusual code is a bad trade in both directions.

Should I minify at all if the output will be protected?

Yes, in almost every case. Minification removes comments, whitespace and dead code, which shrinks the input that protection then expands, so the final artifact is smaller than if you had skipped it. It also strips the banner comments and formatting that make a bundle easy to skim. The two steps are complementary as long as they run in the right order.

Does this ordering change for a library rather than an application?

The principle holds, but the boundary of what you protect changes. A published package has a public interface that consumers depend on, so exported names have to survive, and that constraint is set by your entry points rather than by the minifier. Bundle and minify as usual, then protect with your public surface excluded.

How should this be wired in a real build?

As a discrete step after your bundler's production build and before publishing, driven by a committed configuration rather than by flags typed on the command line. Point it at the finished output directory, keep source maps out of what you deploy, and run your test suite against the protected artifact so the last transformation in the chain is also the one you verified.

Related reading