Workflow
Published
The short answer is no, and it is not a close call. The more interesting question is the one hiding behind it: if protection only ever runs on the release build, when does anybody actually find out that a transform interacts badly with your code? Getting the first answer right and the second one wrong is how teams end up debugging a protection issue at five o’clock on a release day.
The integrations already assume you meant production
This is not a matter of convention. The build plugins encode it.
The Vite plugin declares apply: "build", which is Vite’s own mechanism for a plugin that participates in builds and not in the dev server. Start vite dev and the protection step is simply not in the graph. It also declares enforce: "post", which places it after other plugins’ transforms — the same protect-last ordering rule, expressed in the plugin system rather than in documentation.
The Next.js integration is explicit in the other direction: it checks whether the build context is a development build and returns without applying the plugin if so, unless you pass applyInDevelopment: true. That option exists for the narrow case of reproducing a protection-specific defect, and the README says as much. There is also a target selector for choosing client bundles, server bundles or both, because in a framework with a server runtime “the output” is not one thing.
The webpack plugin hooks asset processing at the size-optimisation stage, which is part of producing a compilation’s final assets. That is inherently a build-output concern, not something the development middleware exercises on each change. The React Native integration goes further and distinguishes development from release output by name.
If you have never configured any of this and never thought about it, the default behaviour is already correct. The failure mode this article is really about is the other one.
Why hot reload and protection cannot both work
It is worth understanding the incompatibility rather than accepting it, because it explains why no amount of configuration fixes it.
Hot module replacement keeps a running application alive and swaps one module underneath it. For that to work, the runtime needs to know which module changed, which module it replaces, and how to re-wire the references — all of which depend on module identity and boundaries staying stable between rebuilds.
Protection is a set of transformations that deliberately destabilise exactly those properties. Identifiers are renamed. Control flow is restructured. Code can be moved between scopes, wrapped, or routed through generated helpers shared across the output. A rebuild with different randomness produces a differently-shaped module even when the source edit was a one-character change. There is no stable seam left to swap along.
Suppose you forced it anyway. Every save would trigger a full protected rebuild, so the fast feedback that justifies the dev server is gone. The page would reload rather than patch, losing application state. Your breakpoints would land in generated code. Your stack traces would name mangled identifiers. React’s component names in the devtools tree would be gone. You would be paying the entire cost of protection continuously to gain nothing, since no user sees your dev server.
The real risk: a protected build nobody has run
So protection lives in the release build. The trap is what that implies if release builds are rare. If the protected artifact is produced once per release and tested manually just before it ships, then every protection-related defect — a renamed member an external API was matching on, a transform interacting with a dynamic property access, a runtime guard misfiring in a particular browser — is discovered under maximum time pressure, in a batch, attributed to no particular commit.
This is a well-known shape. It is the same reason teams stopped saving integration testing for the end. The fix is the same too: make the expensive artifact continuously, cheaply, in a place where failure is informative.
The arrangement that works
Four tiers, each doing one job. None of them slows down the inner loop.
1. Local development: unprotected, always. Dev server, hot reload, real names, real source maps, full devtools. This is the default and requires nothing from you.
2. Every pull request: build protected, run end-to-end against it. The single highest-value change most teams can make. Build the artifact exactly as production does, then point your existing browser-level test suite at it. Unit tests belong on unprotected code, since they exercise internals that protection deliberately rearranges; end-to-end tests belong on the protected artifact, because they assert on behaviour a user can observe, which is precisely the contract protection must preserve. Testing obfuscated JavaScript covers the split in detail.
3. Nightly: the full configuration. If your release configuration includes slower or more aggressive options than the per-PR build, run the complete thing on a schedule with the whole suite behind it. This catches the transform whose interaction only appears with the full option set, without adding minutes to every PR.
4. Release: identical to staging, verified after deployment. Staging must be built exactly like production, protection included — a staging environment that skips the step is validating something you are not shipping. After deployment, confirm that the delivered file’s digest matches the release manifest, because delivery layers rewrite JavaScript more often than people expect.
When something only breaks in the protected build
It will happen occasionally. The instinct is to open the protected output and start reading, which is the least productive available option. Narrow first.
Make it deterministic. Set a fixed seed so repeated builds of the same input produce the same output. Without that you are chasing a target that moves between runs, and you cannot tell a fix from a reshuffle.
Bisect the configuration, not the code. The failure is caused by a transform interacting with something, so the fastest path is to turn transform groups off until it disappears. A handful of builds usually names the responsible option, and that is far more actionable than a location in generated code.
Suspect the external contracts first. The overwhelmingly common cause is renaming something the outside world matches by name: a property a server response is keyed by, a callback a third-party library looks up, a global another script expects to find. Rule-based member protection with those names reserved fixes the whole category.
Symbolicate rather than read. Keep the source map from the protected build, stored privately and never deployed. A production stack trace mapped back to real names and lines answers in seconds what reading generated code answers in an afternoon. See debugging obfuscated JavaScript in production.
A note on source maps
Development and production want opposite things here, and conflating them causes real harm in both directions. In development you want full source maps, always, and there is no downside because nothing is public. For a protected release you still want the map — it is what makes production errors diagnosable — but it must be treated as a build secret: uploaded to your error tracker or archived with the release, never served next to the bundle. A protected bundle with its source map deployed alongside it is publishing the source you just protected, and it is a surprisingly common misconfiguration.
The short version
Do not protect during development; the integrations already default to skipping it, and hot reload and obfuscation want structurally incompatible things from your module graph. Do protect in every build that leaves a developer’s machine, including staging. Put a protected build behind your end-to-end suite on every pull request so breakage is attributed to a commit rather than discovered during a release. And when something does only fail protected, fix the seed, bisect the configuration, check your external name contracts, and symbolicate instead of reading generated code.
Frequently asked questions
Should the protection step run during local development?
No. Protection belongs in production builds, and the build integrations are written so that is what happens without configuration. The Vite plugin declares itself as applying to builds only, so the dev server never invokes it. The Next.js integration skips development builds unless you explicitly pass an option asking for the opposite. Running protection on every file save would make each edit slower, break hot module replacement, and give you stack traces nobody can read while debugging.
Why is hot module replacement incompatible with obfuscation?
Because they depend on opposite properties. Hot replacement swaps one module at runtime while keeping the rest of the application alive, which requires stable module boundaries and stable identities across rebuilds. Protection deliberately renames identifiers, restructures control flow and can move code between scopes, so the thing you would swap in no longer lines up with what is already running. Even where a swap succeeded technically, the debugging experience it produces is the opposite of the one hot reload exists to give you.
When should a protected build first be exercised?
On every pull request, in CI, not at release time. The realistic failure mode is not that protection breaks constantly, it is that when it does break, the team finds out during a release window with no time to investigate. Build the protected artifact in CI and run your end-to-end suite against it. That way any interaction between a transform and your code surfaces attached to the commit that introduced it, while the change is still fresh.
How do I debug a bug that only appears in the protected build?
Narrow it before reading any protected code. Reproduce with a fixed seed so the output is deterministic between runs. Then bisect the configuration rather than the code: disable transform groups until the failure disappears, which usually identifies the responsible option in a handful of builds. Keep the source map from that build stored privately so a production stack trace can be symbolicated back to real names and lines, and prefer a rule-based configuration for anything that touches names an external system depends on.
Is there any reason to protect a development build?
There are two, and both are narrow. The first is a shared preview or demo environment reachable by people outside the team, which is a deployment rather than a dev loop and should be built like a release. The second is deliberately reproducing a protection-specific defect on your own machine, which is a one-off build rather than a mode you leave enabled. Neither is a reason to put protection in the watch loop.
Does protecting only production builds mean staging differs from production?
It does if you let staging skip the step, and that is a mistake worth avoiding. Staging should be built exactly like production, protection included, so the environment you validate is the environment you ship. The distinction that matters is between the inner development loop and every built artifact, not between staging and production. Local development is unprotected; everything you deploy anywhere is protected and identical in construction.
How much time does the protection step add to a build?
It scales with the amount of code and the transforms enabled, and it runs once per release build rather than once per keystroke, which is why the placement matters more than the cost. Keeping it out of the inner loop means developers never pay it. In CI it is a single stage on an already-built artifact. If it becomes a bottleneck, narrow the input to the files that actually need protecting rather than pointing it at an entire output directory including vendor chunks and source maps.
Related reading