死代码消除
死代码消除(DCE)是一种优化技术,它会从你的包中移除未使用的代码,使其体积更小、加载更快。
Rolldown 会移除同时满足以下两个条件的代码:
- 未被使用 - 该值从未被使用
- 没有副作用 - 移除该代码不会改变程序行为
下面是一个简单示例:
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// main.js
import { add } from './math.js';
console.log(add(2, 3));在这个示例中,multiply 从未被导入,并且没有副作用,因此 Rolldown 会将它从最终 bundle 中移除。
Tree-Shaking
Tree-shaking 是一个相关术语,由 Rollup 推广。它指的是一种通过“摇动”语法树来移除未使用代码的特定死代码消除技术。
什么是副作用?
副作用是指任何会影响自身作用域之外内容的操作。常见的副作用包括:
- 修改全局变量或 DOM
- 导入 CSS 文件(会将样式应用到页面)
- 修改原型或全局对象的 polyfill
// 副作用:应用样式
import './styles.css';
// 副作用:修改全局变量
window.API_URL = '/api';
// 副作用:修改原型
Array.prototype.first = function () {
return this[0];
};Rolldown 如何检测副作用
Rolldown 会通过分析以下内容自动检测你的代码是否有副作用:
- 模块是否包含在导入时执行的顶层代码
- 函数调用是否可能修改外部状态
- 属性访问是否可能触发带有副作用的 getter
不过,静态分析有其局限性。有些模式过于动态,无法分析,因此当 Rolldown 不确定时,可能会保守地保留代码。你可以通过 treeshake.unknownGlobalSideEffects 和 treeshake.propertyReadSideEffects 来调整这一行为。
你也可以通过显式标记代码为无副作用,帮助 Rolldown 执行更激进的死代码消除。
将代码标记为无副作用
你可以使用注释标记告诉 Rolldown 某段代码是无副作用的。这些注释默认启用,可通过 treeshake.annotations 关闭。
@__PURE__
@__PURE__ 注释告诉 bundler 某个函数调用没有副作用。如果结果未被使用,整个调用都可以被移除。
const button = /* @__PURE__ */ createButton();如果 button 从未被使用,Rolldown 会完全移除 createButton() 调用。若没有该注释,Rolldown 会保留该调用,因为它无法确定 createButton() 是否没有副作用。
TIP
为了兼容其他工具,这个注释也可以写成 /* #__PURE__ */(使用 # 而不是 @)。
@__NO_SIDE_EFFECTS__
@__NO_SIDE_EFFECTS__ 注释告诉 bundler,该函数声明的任何调用都没有副作用。
/* @__NO_SIDE_EFFECTS__ */
function createComponent(name) {
return {
name,
render() {
return `<${name}></${name}>`;
},
};
}
// 如果 `button` 未使用,这个调用将被移除
const button = createComponent('button');
// 如果 `input` 未使用,这个调用也将被移除
const input = createComponent('input');当你知道函数本身始终是纯函数时,这种方式可能比在每个调用点都添加 @__PURE__ 更方便。
将整个模块标记为无副作用
虽然你可以标记单个表达式或函数,但你也可以将整个模块标记为无副作用。如果你将某个模块标记为无副作用,那么当它的导出都没有被使用时,Rolldown 会将该模块中的每条语句都视为无副作用。
“它的导出都没有被使用”是什么意思?
这里指的是在模块自身中定义的导出,而不是从其他模块重新导出的内容。
// 假设此文件被标记为无副作用
window.loaded = true; // 副作用
// 在此文件中定义 - 计入“它的导出”
export function add(a, b) {
return a + b;
}
// 从另一个文件重新导出 - 这些不计入
export { multiply } from './math.js';
export * from './math2.js';
import { divide } from './math3.js';
export { divide };在这个示例中:
- 如果你
import { add } from './utils.js',则该模块被视为“已使用”,因为add是在utils.js中定义的 - 如果你只
import { multiply } from './utils.js',则该模块被视为“未使用”,因为multiply只是重新导出,并不是在这里定义的
例如,考虑以下情况:
// math.js
window.myGlobal = 'hello'; // 副作用:修改全局变量
export function add(a, b) {
return a + b;
}
// main.js
import './math.js';
console.log('main');如果 math.js 被标记为无副作用,则输出将会是:
console.log('main');这是有条件的
只有当模块的导出都没有被使用时,这些语句才会被视为无副作用。如果任何导出被使用,副作用就会被保留。
示例
例如,考虑以下情况:
// math.js(标记为无副作用)
window.myGlobal = 'hello'; // 副作用:修改全局变量
export function add(a, b) {
return a + b;
}
// main.js
import { add } from './math.js';
console.log('main', add(2, 3));输出将会是:
window.myGlobal = 'hello';
function add(a, b) {
return a + b;
}
console.log('main', add(2, 3));另一方面,如果你将 math.js 中的每条语句都标记为无副作用,则输出将会是:
function add(a, b) {
return a + b;
}
console.log('main', add(2, 3));package.json 中的 sideEffects
package.json 中的 sideEffects 字段会告诉 bundler 你包里的哪些文件具有副作用:
{
"name": "my-library",
"sideEffects": false
}将 sideEffects: false 会把包中的所有文件都标记为无副作用,这在工具库中很常见。
你也可以指定一个包含具有副作用文件的数组:
{
"name": "my-library",
"sideEffects": ["./src/polyfill.js", "**/*.css"]
}这会告诉 Rolldown,大多数文件都没有副作用,在未使用时可以被移除,但 polyfill.js 和 CSS 文件必须保留。
该数组支持 glob 模式(支持 *、**、{a,b}、[a-z])。像 *.css 这样不包含 / 的模式会被视为 **/*.css。
CSS 文件
如果你的库导入了 CSS 文件,请务必将它们包含在 sideEffects 数组中。否则,CSS 导入可能会被移除:
{
"name": "my-component-library",
"sideEffects": ["**/*.css", "**/*.scss"]
}插件钩子:moduleSideEffects
插件可以在 resolveId、load 或 transform 钩子中返回 moduleSideEffects,以覆盖特定模块的副作用检测:
export default {
plugins: [
{
name: 'my-plugin',
resolveId(source) {
if (source === 'my-pure-module') {
return {
id: source,
moduleSideEffects: false,
};
}
return null;
},
},
],
};用于判断模块副作用的优先级顺序为:
transform钩子返回的moduleSideEffectsload钩子返回的moduleSideEffectsresolveId钩子返回的moduleSideEffectstreeshake.moduleSideEffects选项package.json中的sideEffects字段
示例:优化组件库
考虑一个具有如下结构的组件库:
my-component-lib/
├── package.json
└── src/
├── index.js
└── components/
├── Button.js
├── Button.css
├── Modal.js
└── Modal.cssexport { Button } from './components/Button.js';
export { Modal } from './components/Modal.js';import './Button.css';
export function Button(props) {
/* ... */
}为了确保未使用的组件可以被移除,只将 CSS 文件标记为有副作用:
{
"name": "my-component-lib",
"sideEffects": ["**/*.css"]
}现在,当使用者只导入 Button 时:
import { Button } from 'my-component-lib';
render(<Button />);Rolldown 将会:
- 包含
components/Button.js(因为使用了Button) - 包含
components/Button.css(因为它被components/Button.js导入,并且被标记为有副作用) - 排除
components/Modal.js(因为没有使用Modal) - 排除
components/Modal.css(因为components/Modal.js被排除)
