What Your Code Doesnât Use Still Matters: Understanding Tree Shaking in JavaScript
Tree shaking removes unused code from a bundle by statically analyzing ES module imports, but only when it can guarantee that removing that code will not affect program behavior.
At some point, you write less code⌠but your application doesnât get smaller.
You remove functions.
You stop using certain utilities.
And yet, the bundle size barely changes.
It feels strange.
Because intuitively, unused code should disappear.
But in JavaScript, that only happens under certain conditions.
And when those conditions arenât metâŚ
the code stays.
The Illusion of âUnusedâ
Consider this:
import { add, subtract } from "./math"But in your code, you only use:
add(2, 3)So the question becomes:
What happens to subtract?
You might assume itâs gone.
But that assumption depends on something deeperâ
whether the system can prove that itâs safe to remove it.
What Tree Shaking Really Is
Tree shaking is often described as removing unused code.
But that description is incomplete.
A more honest way to see it is:
Removing code that can be proven to have no effect.
That word matters.
Proven.
Because if thereâs even a small chance that removing something could change behaviorâŚ
it stays.
Why Structure Determines What Can Be Removed
This is where ES Modules quietly change everything.
When you write:
import { add } from "./math"Youâre being explicit.
Youâre saying:
âI only need thisâ
And because imports are static, tools can analyze your code before it runs.
They can trace:
- what is imported
- what is never used
And safely remove the rest.
This is what makes tree shaking possible.
When Certainty Disappears
Now compare that with:
import * as math from "./math"At first glance, it feels similar.
You still only call:
math.add(2, 3)But something subtle has changed.
Youâre no longer requesting a specific piece.
Youâre asking for the entire module as a single object.
And once everything is grouped like thatâŚ
certainty disappears.
The Problem With âMaybeâ
From your perspective, itâs obvious that only add is used.
But from the bundlerâs perspective, it has to consider possibilities:
const fn = "add"
math[fn](2, 3)Or:
for (const key in math) {
console.log(math[key])
}Now every export might be accessed.
Not because it isâ
but because it could be.
And that possibility is enough to stop tree shaking.
When Code Becomes Untouchable
Thereâs another layer to this.
Even if a function is never usedâŚ
if the module has side effects, it cannot be removed.
export const value = 1
console.log("hello")That console.log runs when the module is loaded.
So removing the file would change behavior.
Which means it must stay.
Tree shaking doesnât just ask:
âIs this used?â
It asks:
âIs it safe to remove?â
The Deeper Pattern
At this point, something becomes clear.
Tree shaking depends on structure.
Not just what you writeâ
but how predictable your code is.
The more explicit and static your code:
import { add } from "./math"The easier it is to analyze.
The more dynamic and flexible:
import * as math from "./math"The harder it becomes.
And when analysis becomes uncertainâŚ
optimization stops.
A Shift in Perspective
Itâs easy to think of bundlers as tools that simply âremove unused codeâ.
But theyâre closer to something else.
They are cautious interpreters.
They donât assume.
They donât guess.
They only remove what they can guarantee is unnecessary.
A Final Thought
Unused code doesnât disappear just because you donât call it.
It disappears when your code is structured in a way that makes its absence safe.
And that changes how you think about writing JavaScript.
Not just in terms of what worksâ
but in terms of what can be understood.
Because in the end, tree shaking isnât just about removing code.
Itâs about writing code that can be clearly seen for what it is.