Tree shaking

Rspack supports tree shaking, a terminology widely used within the JavaScript ecosystem defined as the removal of unused code, commonly referred to as "dead code." Dead code arises when certain exports from a module are not used and they lack side effects, allowing such pieces to be safely deleted to reduce the final output size.

What is tree shaking

You can imagine your application as a tree. The source code and libraries you actually use represent the green, living leaves of the tree. Dead code represents the brown, dead leaves of the tree that are consumed by autumn. In order to get rid of the dead leaves, you have to shake the tree, causing them to fall.

Note that Rspack does not directly remove dead code but labels unused exports as potential "dead code". These labels can then be recognized and processed by subsequent compression tools. As such, if minimize features are disabled, no actual code removal will be observable.

What is dead code

Dead code refers to a piece of code that is no longer executed, typically due to refactoring, optimization, or logical errors. These codes may be remnants of previous versions or codes that will never be executed under certain conditions.

Prerequisites

To effectively leverage tree shaking, you need to:

  • Set Rspack's mode to production to enable related optimizations.
    • When performing a production build, mode defaults to production.
  • Use ES modules syntax (i.e., import and export).
    • When using compilers like SWC or Babel, ensure they don't transform ES modules syntax to CommonJS.
    • For example, in @babel/preset-env, set the modules option to false.

Configurations

When the mode is set to production, Rspack enables a series of optimizations related to tree shaking, including:

  • usedExports: Checks whether module exports are utilized, allowing the removal of unused exports.
  • sideEffects: Assesses modules for side effects. Modules without side effects can be optimized further via re-exports.
  • providedExports: Analyzes all exports and their sources of re-exportation.
  • innerGraph: Tracks the transmission of variables, enhancing the accuracy of determining whether exports are indeed used.

Below are examples to illustrate how these configuration options function. For the sake of clarity, we will use pseudocode to demonstrate the effects of code removal.

Let's understand this mechanism better through an example, assuming src/main.js as the project's entry point:

src/main.js
import { foo } from './util.js';

console.log(foo);
// `bar` is not used
src/util.js
export const foo = 1;
export const bar = 2;

In this example, bar from util.js is unused. In production mode, Rspack defaults to enabling the usedExports optimization, detecting which exports are actively used. Unused exports, like bar, are safely removed. The final output would resemble:

dist/main.js
const foo = 1;

console.log(foo);

Side effects analysis

In production mode, Rspack also typically analyzes modules for the presence of side effects. If all exports from a module are unused and the module is devoid of side effects, then the entire module can be deleted. Let's modify the previous example a bit:

src/main.js
import { foo } from './util.js';

- console.log(foo);
// `bar` is not used

In this case, none of the exports from util.js are used, and it’s analyzed as having no side effects, permitting the entire deletion of util.js.

You may manually indicate whether a module retains side effects through package.json or module.rules. To to do so, you should enable optimization.sideEffects.

In package.json, you can use true or false to mark whether all modules under the current package contain side effects.

package.json
{
  "name": "package",
  "version": "1.0.0",
  "sideEffects": false
}

The above package.json indicates that all modules under this package are considered side-effect-free.

You can also use glob patterns or string matching to specify specific modules. Unmatched modules will automatically be treated as side-effect-free. Therefore, if manually marking side effects, you must ensure that all unmarked modules are truly free of side effects.

package.json
{
  "name": "package",
  "version": "1.0.0",
  "sideEffects": ["./src/main.js", "*.css"]
}

Above package.json indicates that only ./src/main.js and all .css files are considered to have side effects, while all other modules are treated as side-effect-free.

Re-export analysis

Re-exports are common in development. However, a module might pull in numerous other modules while typically only a fraction of those are needed. Rspack optimizes this situation by ensuring that the referring party can access the actual exported modules directly. Consider this example involving re-exports:

src/main.js
import { value } from './re-exports.js';
console.log(value);
src/re-exports.js
export * from './value.js';
export * from './other.js'; // this can be removed if `other.js` does not have any side effects
src/value.js
export const value = 42;
export const foo = 42; // not used

Rspack defaults to enable providedExports, which can analyze all exports from a re-exporting module and identify their respective origins.

If src/re-exports.js contains no side effects, Rspack can convert the import in src/main.js from src/re-exports.js directly into imports from src/value.js, effectively:

src/main.js
- import { value } from './re-exports.js';
+ import { value } from './value.js';
console.log(value);

This approach benefits by entirely ignoring the src/re-exports.js module.

With an ability to analyze all re-exports in src/re-exports.js, it is determined that foo from src/value.js is not used and will be removed in the final output.

Variable transmission

In some cases, even though exports are accessed, they might not actually be used. For example:

src/main.js
import { foo } from './value.js';

function log() {
  console.log(foo);
} // `log` is not used

const bar = foo; // `foo` is not used

In the scenario above, even though the log function and the variable bar depend on foo, since neither is used, foo can still be considered dead code and be deleted.

After enabling innerGraph optimization (enabled by default for production mode), for complex cross-module situations, Rspack maintains the ability to track variable usage, thereby achieving precise code optimization.

src/main.js
import { value } from './bar.js';
console.log(value);
src/bar.js
import { foo } from './foo.js';
const bar = foo;
export const value = bar;
src/foo.js
export const foo = 42;

In this context, because value is eventually used, the foo it depends on is retained.

Pure annotation

Using the /*#__PURE__*/ annotation to tell Rspack that a function call is side-effect-free (pure) . It can be put in front of function calls to mark them as side-effect-free.

When the initial value in a variable declaration of an unused variable is considered as side-effect-free (pure), it is getting marked as dead code, not executed and dropped by the minimizer.

/*#__PURE__*/ double(55);
TIP
  • Arguments passed to the function are not being marked by the annotation and may need to be marked individually.
  • This behavior is enabled when optimization.innerGraph is set to true.