How Tree Shaking Differs Across Webpack, esbuild, Turbopack, and Rollup

This article provides a concise overview of tree shaking mechanisms in major JavaScript bundlers—Webpack/Rspack, esbuild, Turbopack, and Rollup—explaining their implementation stages, static analysis techniques, optimization trade‑offs, and practical code examples that highlight each tool's strengths and limitations.

ByteDance Web Infra
ByteDance Web Infra
ByteDance Web Infra
How Tree Shaking Differs Across Webpack, esbuild, Turbopack, and Rollup

Tree shaking has become an essential part of modern front‑end bundling. Because each bundler focuses on different use cases, their tree‑shaking implementations vary: Webpack emphasizes correctness for application bundles, while Rollup targets library bundles with finer‑grained AST‑level optimizations.

What is tree shaking? Imagine your application as a tree: green leaves are the code actually used, gray leaves are dead code. Shaking the tree removes the dead leaves.

Tree shaking in Webpack / Rspack

Rspack (v1.4) follows the same tree‑shaking principles as Webpack, and the article uses Webpack as the reference implementation.

Webpack’s tree shaking consists of three parts:

module‑level: optimization.sideEffects removes modules with no used exports and no side effects. import "./module" – no used exports and no side effects, can be removed.

Barrel files that re‑export unused modules can also be removed.

export‑level: optimization.providedExports and optimization.usedExports delete unused exports. optimization.providedExports analyzes which exports a module provides. optimization.usedExports tracks which exports are actually used; unused exports are omitted during code generation.

code‑level: optimization.minimize uses minifiers (SWC, Terser) to perform inline analysis, dead‑code removal, and compression.

Static analysis is crucial: module‑level and export‑level rely on Webpack’s JavaScript parser to determine side effects and export usage. Currently, Webpack’s handling of CommonJS and dynamic imports is limited, leaving room for improvement.

Even after these stages, some cases remain problematic:

Export a used by another unused export g prevents a from being shaken; optimization.innerGraph can analyze top‑level statement dependencies to resolve this.

Minifiers cannot optimize code wrapped in Webpack’s runtime functions, e.g., long constant names; enabling optimization.concatenateModules can help, though it works only in simple scenarios.

When minifiers cannot mangle exported variables due to the runtime wrapper, optimization.mangleExports is needed.

Tree shaking in esbuild

esbuild’s tree shaking follows four steps:

Split each module into top‑level statements (parts).

Analyze variable definitions and usage across parts, linking imports to corresponding exports.

Traverse from entry parts, marking a part as live ( IsLive = true) if it is used or has side effects.

Generate code only for live parts; dead parts are discarded.

For more details, see esbuild’s official documentation.

By splitting at the top‑level, esbuild naturally avoids the inner‑graph problem that Webpack faces, allowing part‑level analysis and optimization.

Earlier versions of esbuild also supported module splitting, but this feature was dropped because handling top‑level await across chunks proved difficult.

Tree shaking in Turbopack

Turbopack uses a Webpack‑like output format where each module is wrapped in a function, separating loading from execution. It adds a setter ( __turbopack_context__.s) alongside the getter to keep state updates correct.

// Turbopack simplified example
module.exports = {
  "entry1.js": (__turbopack_context__) => {
    var _data_1 = __turbopack_context__.i("data.js <part 1>");
    console.log(_data_1.data);
  },
  "entry2.js": (__turbopack_context__) => {
    var _data_2 = __turbopack_context__.i("data.js <part 2>");
    _data_2.setData(123);
  },
  "data.js <part 2>": (__turbopack_context__) => {
    __turbopack_context__.s({
      setData: () => setData,
    });
    var _data_1 = __turbopack_context__.i("data.js <part 1>");
    function setData(value) { _data_1.data = value; }
  }
};

Top‑level await is handled with a special runtime ( __turbopack_context__.a) to ensure correct execution order.

// Top‑level await handling example
module.exports = {
  "data.js <part 0>": (__turbopack_context__) => {
    __turbopack_context__.a(async (__turbopack_handle_async_dependencies__, __turbopack_async_result__) => {
      try {
        await 1; // added await
        __turbopack_async_result__();
      } catch (e) { __turbopack_async_result__(e) }
    }, true);
  }
};

Drawbacks of Turbopack’s approach include increased bundle size due to many wrapper functions and slower property access via objects like _data_0__TURBOPACK_MODULE__.data, which may affect performance.

Tree shaking in Rollup

Rollup performs a top‑down analysis from the entry point, traversing the AST node by node, checking side effects, and including only necessary nodes. Its finer‑grained AST‑level analysis often yields smaller bundles than other bundlers.

Call include() on the module.

Starting from top‑level AST nodes, determine side effects, include the node if needed, and recursively process related nodes.

After traversal, if new nodes were included, repeat the process.

During step 2.c, related nodes include child nodes, variable declarations, and property accesses.

Advantages of Rollup’s approach:

Finer granularity: AST‑node level analysis removes dead code more precisely than module‑level approaches.

More precise side‑effect analysis at the AST level, allowing context‑aware decisions.

Examples illustrate Rollup’s capabilities:

1. Cross‑module dead code elimination

// index.js
import { f } from "./lib";
f();
// lib.js
import { a } from "./module";
export const f = () => 42;
export const g = () => a; // g unused, prevents a from being shaken
// module.js
export const a = 42;

2. Removing unused object properties

// code example
const obj = { a: { ab: 1 }, unused: 2 };
console.log(obj.a.ab);

Rollup keeps only the a and ab properties, discarding the rest.

3. Side‑effect detection via reassignment

// index.js
import { a } from "./module";
export const f = () => 42;
export const g = () => a; // g unused, a considered used
// module.js
export const a = 42;

Rollup treats a.b = 3 as having side effects if a is reassigned elsewhere, even if the actual value does not change.

Rollup v3 supports statement‑level shaking; v4 introduces AST‑node level shaking, default‑parameter analysis, and other experimental optimizations.

Tree shaking of function default parameters (reverted due to bailouts).

Dead‑branch removal based on constant function arguments.

These enhancements enable more aggressive dead‑code removal across a wider range of scenarios.

code optimizationWebpackTree ShakingRollupesbuildturbopackfrontend bundlers
ByteDance Web Infra
Written by

ByteDance Web Infra

ByteDance Web Infra team, focused on delivering excellent technical solutions, building an open tech ecosystem, and advancing front-end technology within the company and the industry | The best way to predict the future is to create it

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.