CC 4.0 协议声明

本节内容派生于以下链接指向的内容 ,并遵守 CC BY 4.0 许可证的规定。

以下内容如果没有特殊声明,可以认为都是基于原内容的修改和删减后的结果。

Tree shaking

Rspack 支持 tree shaking,这是在 JavaScript 上下文中常用的术语,用于描述死代码的删除。

它依赖于 import 和 export 语句来检测代码模块是否被导出和导入以在 JavaScript 文件之间使用。

当你将 mode 设置为 production 时,Rspack 将默认启用 tree shaking。

Basic tree shaking

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'development',
  entry: {
    index: './src/index.js',
  },
  output: {},
};

module.exports = config;
./src/index.js
import { cube } from './math.js';

function component() {
  const element = document.createElement('pre');

  element.innerHTML = ['Hello webpack!', '5 cubed is equal to ' + cube(5)].join(
    '\n\n',
  );

  return element;
}

document.body.appendChild(component());
./src/math.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

注意到,我们没有从 src/math.js 模块导入 square 方法。该函数是“死代码”,表示应该被删除的未使用的导出。现在构建我们的项目 将产生以下构建结果:

// ... 省略一些不重要的代码
var __webpack_modules__ = {
  './src/index.js': function (module, exports, __webpack_require__) {
    'use strict';
    Object.defineProperty(exports, '__esModule', {
      value: true,
    });
    var _mathJs = __webpack_require__('./src/math.js');
    function component() {
      const element = document.createElement('pre');
      element.innerHTML = [
        'Hello webpack!',
        '5 cubed is equal to ' + (0, _mathJs.cube)(5),
      ].join('\n\n');
      return element;
    }
    document.body.appendChild(component());
  },
  './src/math.js': function (module, exports, __webpack_require__) {
    'use strict';
    Object.defineProperty(exports, '__esModule', {
      value: true,
    });
    function _export(target, all) {
      for (var name in all)
        Object.defineProperty(target, name, {
          enumerable: true,
          get: all[name],
        });
    }
    _export(exports, {
      square: function () {
        return square;
      },
      cube: function () {
        return cube;
      },
    });
    function square(x) {
      return x * x;
    }
    function cube(x) {
      return x * x * x;
    }
  },
};
// ...

正如你所看到的,如果我们不启用 tree shaking,所有代码都保持不变,只是将代码包裹了一层运行时代码。

现在,我们切换到 production 模式,重新构建项目,为了让产物更加可读,我们同时关闭 minimize 选项 以及切换 moduleIdsnamed,为了和后面章节对比,我们关闭 optimization.sideEffects

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'production',
  entry: {
    index: './src/index.js',
  },
+ optimization: {
+   sideEffects: false,
+   moduleIds: 'named',
+   minimize: false
+ }
};

module.exports = config;

重新构建项目后,square 函数将会被删除。

SideEffects

在一个 100% ESM 模块化的世界里,识别副作用是比较直接的。然而,我们还没有达到那个阶段 (在实际的生产代码中有各种格式的代码混用,cjs、esm 和 umd 等等),所以在此期间,需要在你的代码中提供“纯度”方面的提示给 Rspack 的编译器。 该功能通常由 "sideEffects" package.json 属性来完成的。

package.json
{
  "name": "your-project",
  "sideEffects": false
}

sideEffects 字段支持以下值:

  • false 这个包中的所有文件都没有副作用。
  • string 匹配包含副作用文件的 glob。
  • Array<string> 匹配包含副作用文件的 glob 数组。
  • undefined 当你不设置 package.jsonsideEffects 时的默认值。当 optimization.sideEffects 为 true 时,Rspack 将尝试分析代码是否具有副作用,当 optimization.sideEffects 为 'flag' 时,Rspack 将默认包中的所有模块均有副作用。

这次,我们使用一个更复杂的示例。

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'production',
  entry: {
    index: './src/index.js',
  },
  optimization: {
    moduleIds: 'named',
    minimize: false,
  },
};
module.exports = config;
index.js
import { multiply } from 'math';

console.log(multiply(2, 3));
node_modules/math/package.json
{
  "name": "math",
  "sideEffects": false
}
node_modules/math/index.js
export * from './add.js';
export * from './multiply.js';
export * from './subtract.js';
node_modules/math/subtract.js
export const subtract = (a, b) => a - b;
node_modules/math/multiply.js
export const multiply = (a, b) => a * b;
node_modules/math/add.js
const randomDate = Date.now();
export const addRandomDate = a => a + randomDate;
export const add = (a, b) => a + b;

变量 randomDate 在默认情况下是需要被保留的,因为它在模块初始化时期包含副作用。 但是,由于 package.json 中包含了 sideEffects 字段,且值为 false,除此之外在 add.js 中没有使用任何导出变量,因此整个模块都可以被跳过,subtract.js 同理。

//...
var __webpack_modules__ = {
  './node_modules/math/index.js': function (
    module,
    exports,
    __webpack_require__,
  ) {
    'use strict';
    Object.defineProperty(exports, '__esModule', {
      value: true,
    });
    __webpack_require__.es(
      __webpack_require__('./node_modules/math/multiply.js'),
      exports,
    );
  },
  './node_modules/math/multiply.js': function (
    module,
    exports,
    __webpack_require__,
  ) {
    'use strict';
    Object.defineProperty(exports, '__esModule', {
      value: true,
    });
    Object.defineProperty(exports, 'multiply', {
      enumerable: true,
      get: function () {
        return multiply;
      },
    });
    const multiply = (a, b) => a * b;
  },
  './src/index.js': function (module, exports, __webpack_require__) {
    'use strict';
    Object.defineProperty(exports, '__esModule', {
      value: true,
    });
    var _indexJs = __webpack_require__('./node_modules/math/index.js');
    console.log((0, _indexJs.multiply)(2, 3));
  },
};
// ...

module.rule.sideEffects

你可以使用 module.rule.sideEffects 覆盖某些模块的 sideEffects 选项。

为什么我们需要这样的功能呢?我们仍然使用上面的例子,假设 math 包的作者忘记在 package.json 中添加 sideEffects 选项:

node_modules/math/package.json
{
+ "name": "math"
- "name": "math",
- "sideEffects": false
}

Rspack 将尝试安全地分析代码,并仅在所有顶级语句均没有副作用时标记模块为无副作用。

正如我们所看到的,math/index.jsmath/subtract.jsmath/multiply.js 都没有副作用,而 math/add.js 不是,因为 const randomDate = Date.now() 含有副作用。当我们重新构建项目时,你可以看到差异如下:

//...
var __webpack_modules__ = {
"./node_modules/math/index.js": function (module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
__webpack_require__.es(__webpack_require__("./node_modules/math/multiply.js"), exports);
+__webpack_require__.es(__webpack_require__("./node_modules/math/add.js"), exports);
},
"./node_modules/math/multiply.js": function (module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "multiply", {
    enumerable: true,
    get: function() {
        return multiply;
    }
});
const multiply = (a, b)=>a * b;
},
+"./node_modules/math/add.js": function (module, exports, __webpack_require__) {
+ "use strict";
+ Date.now()
+},
"./src/index.js": function (module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
var _indexJs = __webpack_require__("./node_modules/math/index.js");
console.log((0, _indexJs.multiply)(2, 3));
},

}
// ...

由于module.rule.sideEffects 比 package.json 中的 sideEffects 优先级更高,我们可以使用 module.rule.sideEffects 来覆盖某些模块的 sideEffects 标识. 由于初始化的副作用只在使用 addRandomDate 时才有意义,因此我们可以安全地覆盖它。为此,我们可以对 rspack.config.js 进行以下修改:

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'production',
  entry: {
    index: './src/index.js',
  },
  optimization: {
    minimize: false,
    moduleIds: 'named'
  },
+  module: {
+    rules: [
+      {
+        test: /math\/add\.js/,
+        sideEffects: false
+      }
+    ]
+  }
};
module.exports = config;

重新构建项目,我们得到和之前相同的结果,整个 math/add.js 模块都被删除了。

Reexports optimization

开启 SideEffects 优化后,Rspack 还会尝试对重导出模块进行优化。

sdk.js
export { a } from './a.js';
export { b } from './b.js';
export { c } from './c.js';
// ...
index.js
import { a } from './sdk.js';
console.log(a);

sdk.js 无副作用时,Rspack 会尝试直接从 a.js 中引入 a,而不引入 sdk.js 中重导出的其他模块。

在某些不规范的库中可能存在循环依赖,例如 a.jssdk.js 中引入 b.js 中的成员。

a.js
import { b } from './sdk.js';
export const a = b;
index.js
import { a } from './sdk.js';
console.log(a);

此时 a.jssdk.js 之间存在循环依赖,很不推荐有意这样利用循环依赖,但仍然可以构建成功,并且成功运行。这是因为 Rspack 会尝试将上述的引用转换成:

a.js
- import { b } from './sdk.js';
+ import { b } from './b.js';
export const a = b;
index.js
- import { a } from './sdk.js'
+ import { a } from './a.js'
console.log(a);

转换后循环依赖没有了。但是如果你尝试关掉 optimization.providedExportsoptimization.sideEffects,构建会成功,但运行时会因为循环依赖而遇到错误。

上述循环依赖的例子可以在这里找到。

这张图演示了重导出优化的过程: