PostHole
Compose Login
You are browsing eu.zone1 in read-only mode. Log in to participate.
rss-bridge 2019-08-04T00:00:00+00:00

How Does the Development Mode Work?

Dead code elimination by convention.


How Does the Development Mode Work?

August 4, 2019

Pay what you like

If your JavaScript codebase is even moderately complex, you probably have a way to bundle and run different code in development and production.

Bundling and running different code in development and production is powerful. In development mode, React includes many warnings that help you find problems before they lead to bugs. However, the code necessary to detect such mistakes often increases the bundle size and makes the app run slower.

The slowdown is acceptable in development. In fact, running the code slower in development might even be beneficial because it partially compensates for the discrepancy between fast developer machines and an average consumer device.

In production we don’t want to pay any of that cost. Hence, we omit these checks in production. How does that work? Let’s take a look.


The exact way to run different code in development depends on your JavaScript build pipeline (and whether you have one). At Facebook it looks like this:

if (__DEV__) {
doSomethingDev();
} else {
doSomethingProd();

Here, __DEV__ isn’t a real variable. It’s a constant that gets substituted when the modules are stitched together for the browser. The result looks like this:

// In development:
if (true) {
doSomethingDev(); // 👈
} else {
doSomethingProd();

// In production:
if (false) {
doSomethingDev();
} else {
doSomethingProd(); // 👈

In production, you’d also run a minifier (for example, terser) on the code. Most JavaScript minifiers do a limited form of dead code elimination, such as removing if (false) branches. So in production you’d only see:

// In production (after minification):
doSomethingProd();

(Note that there are significant limits on how effective dead code elimination can be with mainstream JavaScript tools, but that’s a separate topic.)

While you might not be using a __DEV__ magic constant, if you use a popular JavaScript bundler like webpack, there’s probably some other convention you can follow. For example, it’s common to express the same pattern like this:

if (process.env.NODE_ENV !== 'production') {
doSomethingDev();
} else {
doSomethingProd();

That’s exactly the pattern used by libraries like React and Vue when you import them from npm using a bundler. (Single-file <script> tag builds offer development and production versions as separate .js and .min.js files.)

This particular convention originally comes from Node.js. In Node.js, there is a global process variable that exposes your system’s environment variables as properties on the process.env object. However, when you see this pattern in a front-end codebase, there isn’t usually any real process variable involved. 🤯

Instead, the whole process.env.NODE_ENV expression gets substituted by a string literal at the build time, just like our magic __DEV__ variable:

// In development:
if ('development' !== 'production') { // true
doSomethingDev(); // 👈
} else {
doSomethingProd();

// In production:
if ('production' !== 'production') { // false
doSomethingDev();
} else {
doSomethingProd(); // 👈

Because the whole expression is constant ('production' !== 'production' is guaranteed to be false), a minifier can also remove the other branch.

// In production (after minification):
doSomethingProd();

Mischief managed.


Note that this wouldn’t work with more complex expressions:

let mode = 'production';
if (mode !== 'production') {
// 🔴 not guaranteed to be eliminated

JavaScript static analysis tools are not very smart due to the dynamic nature of the language. When they see variables like mode rather than static expressions like false or 'production' !== 'production', they often give up.

Similarly, dead code elimination in JavaScript often doesn’t work well across the module boundaries when you use the top-level import statements:

// 🔴 not guaranteed to be eliminated
import {someFunc} from 'some-module';

if (false) {
someFunc();

So you need to write code in a very mechanical way that makes the condition definitely static, and ensure that all code you want to eliminate is inside of it.


For all of this to work, your bundler needs to do the process.env.NODE_ENV replacement, and needs to know in which mode you want to build the project in.

A few years ago, it used to be common to forget to configure the environment. You’d often see a project in development mode deployed to production.

That’s bad because it makes the website load and run slower.

In the last two years, the situation has significantly improved. For example, webpack added a simple mode option instead of manually configuring the process.env.NODE_ENV replacement. React DevTools also now displays a red icon on sites with development mode, making it easy to spot and even report.

[Development mode warning in React DevTools]

Opinionated setups like Create React App, Next/Nuxt, Vue CLI, Gatsby, and others make it even harder to mess up by separating the development builds and production builds into two separate commands. (For example, npm start and npm run build.) Typically, only a production build can be deployed, so the developer can’t make this mistake anymore.

There is always an argument that maybe the production mode needs to be the default, and the development mode needs to be opt-in. Personally, I don’t find this argument convincing. People who benefit most from the development mode warnings are often new to the library. They wouldn’t know to turn it on, and would miss the many bugs that the warnings would have detected early.

Yes, performance issues are bad. But so is shipping broken buggy experiences to the end users. For example, the React key warning helps prevent bugs like sending a message to the wrong person or buying the wrong product. Developing with this warning disabled is a significant risk for you and your users. If it’s off by default, then by the time you find the toggle and turn it on, you’ll have too many warnings to clean up. So most people would toggle it back off. This is why it needs to be on from the start, rather than enabled later.

Finally, even if development warnings were opt-in, and developers knew to turn them on early in development, we’d just go back to the original problem. Someone would accidentally leave them on when deploying to production!

And we’re back to square one.

Personally, I believe in tools that display and use the right mode depending on whether you’re debugging or deploying. Almost every other environment (whether mobile, desktop, or server) except the web browser has had a way to load and differentiate development and production builds for decades.

Instead of libraries coming up with and relying on ad-hoc conventions, perhaps it’s time the JavaScript environments see this distinction as a first-class need.


Enough with the philosophy!

Let’s take another look at this code:

if (process.env.NODE_ENV !== 'production') {
doSomethingDev();
} else {
doSomethingProd();

You might be wondering: if there’s no real process object in front-end code, why do libraries like React and Vue rely on it in the npm builds?

[...]


Original source

Reply