A React app with a public part and preview screens behind an environment flag. Flag off in production, screens unreachable — and the build output still contains a 375 kB chunk holding the entire preview. Not a bug in Vite. The bug is in how we wrote the check.
How the Bundler Removes Dead Code
At build time Vite literally replaces import.meta.env.VITE_SOMETHING with the value from the environment. If the code says import.meta.env.VITE_PREVIEW !== ‘true’, what remains after substitution is ‘false’ !== ‘true’ — an expression that is always false. The bundler sees that, declares the branch dead, and the dynamic import inside it never becomes a chunk of its own. That is the only mechanism by which a flag actually removes code from the build.
Where We Broke It
With the best intentions we pulled every flag into one module: const FEATURES = { previewMocks: import.meta.env.VITE_PREVIEW === ‘true’ }, and wrote if (FEATURES.previewMocks) in the routes. More readable, in one place — and unremovable. The value now arrives through an object property from another module, and at the moment the bundler decides about chunks it can no longer prove that branch dead. The route stays, the lazy import stays, the chunk gets generated and deployed.
The Rule We Adopted
A flag that decides whether code exists in the build at all is written inline, right at the branch point, as an import.meta.env expression. The centralised module stays for flags that change the behaviour of code that is already included. The difference is in the question being asked: “should this work?” versus “should this be here at all?”
How to Verify Instead of Trust
The chunk list at the end of the build is the only proof. If a file named after the preview module is still there with the flag off, the branch is not dead, whatever the code “says”. That check belongs in the deploy script: a build that produces a chunk that must not exist should fail, not pass.
A flag the bundler cannot read as a constant is not a flag — it is just a condition evaluated in production.