Back to topics

sideEffects and Tree Shaking Loss Issues

Background

After introducing lodash into the project, analysis of the bundle revealed that lodash was too large in size.

To optimize the bundle size, we decided to enable tree shaking by configuring the sideEffects field in package.json.

However, after enabling tree shaking, two issues appeared:

  1. CSS styles were missing
  2. Some JS modules were not executed

Solution

Solution for missing CSS styles

Mark CSS files as side effects in package.json:

"sideEffects": [
    "**/*.css"
]

Solution for unexecuted JS modules

For modules that do not export anything (i.e., no export) but need to be executed, add them to the sideEffects configuration:

"sideEffects": [
    "./src/utils/resetFontSize.ts"
]

Principles of Tree Shaking

The sideEffects field is used to mark which files have side effects. When performing tree shaking, the bundler generally follows these steps:

  1. Perform static analysis based on ESModule to identify unreferenced modules.
  2. Determine whether the module has side effects; if not, it can be safely removed.
  3. How to determine if a module has side effects?

    On one hand, terser (which webpack depends on) detects side effects in statements (but since JS is a dynamically typed language, in many cases the result cannot be analyzed without executing the code, so statement-level side effect analysis is limited. It can be assisted by magic comments /*#__PURE__*/ to indicate that the declaration has no side effects).

    On the other hand, we can tell the bundler which files have side effects via the sideEffects configuration option, thus allowing the bundler to skip analysis of the entire module and its subtree. In many cases, static analysis cannot accurately determine side effects, so magic comments /*#__PURE__*/ can be used to mark code without side effects.

Practical Example

Here is a typical problem example:

// resetFontSize.ts
import { detectDeviceType } from './deviceUtils';

(function flexible(window: Window, document: Document) {
    function resetFontSize() {
        const clientWidth = parseInt(
            document.documentElement.clientWidth.toString(),
            10
        );
        let size = 0;
        // Use the detectDeviceType function to determine the device type
        if (detectDeviceType() === 'desktop') {
            size = (document.documentElement.clientWidth / 1920) * 16;
            document.documentElement.style.fontSize = (size <= 14 ? 13 : size) + 'px';
        } else {
            size = clientWidth;
            const fontSize = (size / 750) * 16;
            document.documentElement.style.fontSize = fontSize + 'px';
        }
    }

    resetFontSize();
    window.addEventListener('pageshow', resetFontSize);
    window.addEventListener('resize', resetFontSize);
})(window, document);

// index.ts
import '@utils/resetFontSize';

Although index.ts imports resetFontSize, because the module has no export, tree shaking will remove it. The solution is to declare it in sideEffects:

// package.json
"sideEffects": [
    "./src/utils/resetFontSize.ts"
]

Special handling for Vue projects

For Vue projects, if a component contains styles, special treatment is required:

// package.json
"sideEffects": [
    "**/*.css",
    // Mark Vue components that contain styles
    "./src/ComponentWithStyle.vue"
]

Alternatively, you can choose to completely disable tree shaking.

Best Practices

To better leverage tree shaking:

  1. Prefer using package versions that support ES modules, e.g., use lodash-es instead of lodash.
  2. For necessary side-effect code, explicitly declare it in sideEffects.
  3. Use /*#__PURE__*/ comments appropriately to mark code without side effects.