Extreme Dead Code Elimination

Sachin Tyagi
Engineering @ Housing/Proptiger/Makaan
6 min readMar 16, 2023

Part 1: Separate Desktop & Mobile Builds

TL;DR

What is dead code?

Dead code can be described as any piece of code which will not get executed or is not reachable. However there are tools like Terser, which are used to identify such pieces of code and remove them. As an example consider the following code block

Dead code example

In the above example, if we replace __MOBILE__ with true on line 18 and process it with terser [with mangle: false; just so that we get a readable output], we’ll get the following result:

Dead code removed by terser [still has unused imports]

Scroll down to the bottom of this article [or click here] to find a codesandbox embed, where you can play around with this example. Try changing the input code or the global_defs value [try making global_defs.__MOBILE__ = false] to see how the output changes.

Notice that terser removed the unused code branch [line 23–27 in the original example] & it also removed the constant declarations on [line 16] and [line 9–13]. This is because terser was statically able to identify that if(true) will always run.

Terser though couldn’t remove the unused imports, which can be done by webpack’s [or any other good bundler] tree shaking. And the final code can be reduced to the following [line 1–6 removed]:

Final tree shaken code

Isn’t that awesome. This means so much less code being shipped to users browser. But if everything is already taken care of, why are you reading this article? Well, turns out that things are not this easy.

There are two problems

  1. Order of Execution [Tree shaking then terser]:
    In most webpack configurations terser runs after webpack has already done tree shaking process. This creates a problem, since until terser has done it’s work webpack won’t know that the imports are useless [these imports become useless only after terser is done processing]. Hence the imports in the above example won’t be tree shaken by webpack, therefore, the final output will still contain those unused imports.
  2. Static Identification of dead code:
    How do we statically identify unreachable code. In a typical production code we might not have constants for things like __MOBILE__, rather we might be dependent on user-agent to determine things like that

Let’s discuss each of these two problems in detail.

Problem 1: Order Of Execution [Tree Shaking then terser]

Webpack runs TerserJsPlugin in production mode. But the order of their execution is: Tree shaking followed by terser’s minification. This is inefficient.

Let’s look at the sample code snippet above once again. If webpack were to directly attempt tree shaking on the above code. It won’t be able to identify the imports of ComponentA, ComponentB, ComponentC, SomeWrapper to be unused. Because it was terser which was removing the dead code and hence making those imports unused.

Solution? Run terser before webpack. In our case we ended up creating a custom webpack loader like so:

Webpack Loader to remove the dead code

We ran terser on all our .js/.jsx/.ts/.tsx files before webpack could get a chance to process them. Therefore, till the time webpack’s tree shaking started, it was working on a piece of code which was:

Output of Terser

And hence, webpack could easily tree shake the unused imports. [Well obviously webpack needs to be configured properly to do that, check out webpack’s tree shaking guide]

There’s a catch here though. You’ll still want to run terser again after webpack [like it traditionally does]. Because webpack also adds a lot of scaffolding code which you will want to minify. But, that means increased build time. Therefore in the first run of terser we disable most of the optimisations [which will help reduce the time terser takes] and just enable the options[compress] which help us do dead code elimination.

Problem 2: Static Identification of Dead Code

This problem is much more complicated. Why? Well because terser needs to know __MOBILE__ will be false/true at the build time. So the very intuitive solution is to make 2 builds. One for desktop and other for mobile. and somehow define the __MOBILE__ variable at build time.

A very easy solution for that would be something like webpack’s DefinePlugin. But, since we are running terser before webpack could even get a chance, this won’t help us. Instead we could use terser’s global_defs option. like so:

global_defs added to terser options

This works very similar to webpack’s DefinePlugin. Terser will replace all usages of __MOBILE__ with true and do dead code elimination.

We can create 2 builds. and change the value of __MOBILE__ accordingly. And, in this case your mobile builds will benefit from dead code elimination and tree shaking.

  1. mobile.js — to target the mobile users
  2. desktop.js — to target the desktop users

Simple enough, right? Well yes, if you are just doing client side rendering.

If you are into SSR though, the problem is far from solved.

Should we create 2 builds for server as well? and then run 2 servers as well, one for desktop other for mobile. Seems like too much effort, and a lot of infra as well.

Wouldn’t it be nice to have just 1 single server build and 2 client builds? Because ultimately it’s the users’ performance that we want to increase. It really doesn’t matter if our server has unused code.

How to solve this then? Let’s consider the following code:

Simpler Example of Dead Code Branch

We want client-mobile code to become:

Dead Code Eliminated Example for mobile build

and our client-desktop code to become:

And what if our server side code could become something like:

Won’t this solve all our problems? Our client builds are optimised and our single server build has a function call which will be resolved at runtime [figure how? let’s come to this later]. This way a single server build will be able to serve both mobile and desktop users.

If we can:

  1. transform our code like this
  2. make __isMobile function somehow work properly

we’ll be so happy. Let’s solve both of these.

Sub Problem 1: Transform code

We created a babel plugin. Which does the following:

  1. Find all Identifiers named __MOBILE__and replace them with __isMobile function call
  2. Add an import for __isMobile from utils/isMobile

Here’s how a simpler version of that babel plugin looks. [the actual one is a bit more generic and handles a few edge cases, but this version should be enough for the purpose of this article]

Babel plugin for replacing __MOBILE__ to __isMobile() & adding an import

You can use astexplorer.net to write/debug your babel plugins. And refer to plugin handbook for documentation on how to write one

Sub Problem 2: make the __isMobile function work properly

While initially reading this you’ll think what’s the problem here? This should be simple.

We can read the user-agent, parse it [maybe using npm packages like express-useragent] and you are done.

Well the problem is you can only read the user-agent from the request object. And while the babel plugin can replace __MOBILE__ with __isMobile() . We still don’t have the access to the request object. And without the access to request we won’t be able to determine the device correctly.

Well we were truly stuck. That’s when we stumbled upon a npm-package called cls-hooked [not going into how this package works as that can be an article in itself]. This was a solution to all our problems. Using this package like so:

Add the ‘expressMiddleware’ in your express middleware chain, just after express-useragentmiddleware [so that req.useragent is set]

That’s all. This will ensure that your user-agent information is stored in a context [which is specific to a request]. Therefore, whenever a request comes in from a desktop device a call to __isMobile will return false and vice versa.

Pheww!! That was long. But hey’ this works and works well. While the example here is just for desktop/mobile. This approach can be generic and might to customised according to different use-cases.

This approach along with many others has helped us at Housing.com to build beautiful experiences for our users.

Thanks for reading!

This is a first in the series of articles on what we have been doing to improve web performance at Housing.

Check out the other articles of this series: Part 2. Part 3 coming soon!

A shout out to everyone who made this possible: sachin agrawal, Naman Saini, Dhruvil Patel, Nikhil Verma

Below is the codesandbox embed for you to play around with terser. If this doesn’t work for some reason try clicking here

Code sandbox embed to try terser

--

--