无用代码消除
无用代码消除(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 会将其从最终产物中移除。
摇树优化
摇树优化是由 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__ 注解告诉打包器,某个函数调用或 new 表达式没有副作用。如果结果未使用,可以移除整个调用。
const button = /* @__PURE__ */ createButton();
const widget = /* @__PURE__ */ new Widget();如果从未使用 button 和 widget,Rolldown 会彻底移除这两个调用。没有注解时,Rolldown 无法确定 createButton() 和 new Widget() 没有副作用,因此会保留它们。
注解必须出现在调用或 new 表达式的紧邻前方才会生效。如果放在其他位置,Rolldown 会生成 INVALID_ANNOTATION 警告。
常见的无效位置
// 位于非调用表达式之前
/* @__PURE__ */ globalThis.createElement;
// 位于声明之前
/* @__PURE__ */ function foo() {}
// 位于变量声明符的标识符与 `=` 之间
const foo /* @__PURE__ */ = bar();TIP
为了兼容其他工具,注解也可以写成 /* #__PURE__ */(使用 # 代替 @)。
@__NO_SIDE_EFFECTS__
@__NO_SIDE_EFFECTS__ 注解告诉打包器,对该函数声明的任何调用都没有副作用。
/* @__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 字段告诉打包器,包中的哪些文件具有副作用:
{
"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钩子返回的moduleSideEffects。load钩子返回的moduleSideEffects。resolveId钩子返回的moduleSideEffects。treeshake.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被排除)。