Graph · Module · Chunk · ExpressionEntity
2026-03-14 | 构建工具深度解读
Rollup 是下一代 JavaScript 模块打包器,以 Tree Shaking 著称
| 概念 | 说明 |
|---|---|
| Graph | 依赖图,管理所有模块和依赖关系 |
| Module | 单个模块,包含 AST、导入导出信息 |
| Chunk | 代码块,最终输出的代码单元 |
| ExpressionEntity | 表达式实体,包含 include/deoptimize 方法 |
核心思想:通过静态分析标记已使用代码,剔除未使用部分
export default class Graph {
readonly modulesById = new Map<string, Module | ExternalModule>();
entryModules: Module[] = [];
needsTreeshakingPass = false;
readonly newlyIncludedVariableInits = new Set<ExpressionEntity>();
async build(): Promise<void> {
await this.generateModuleGraph();
this.sortAndBindModules();
this.includeStatements(); // 核心:标记已使用代码
}
private async generateModuleGraph(): Promise<void> {
// 加载入口模块
const { entryModules } = await this.moduleLoader.addEntryModules(...);
// 构建依赖图
for (const module of this.modulesById.values()) {
if (module instanceof Module) {
this.modules.push(module);
}
}
}
}
private includeStatements(): void {
const entryModules = [...this.entryModules, ...this.implicitEntryModules];
// 标记入口模块已执行
for (const module of entryModules) {
markModuleAndImpureDependenciesAsExecuted(module);
}
if (this.options.treeshake) {
let treeshakingPass = 1;
do {
this.needsTreeshakingPass = false;
for (const module of this.modules) {
if (module.isExecuted) {
module.include(); // 标记已使用代码
}
}
// 第一轮后导出所有入口导出
if (treeshakingPass === 1) {
for (const module of entryModules) {
if (module.preserveSignature !== false) {
module.includeAllExports();
this.needsTreeshakingPass = true;
}
}
}
} while (this.needsTreeshakingPass); // 迭代直到稳定
}
}
export default class Module {
readonly importDescriptions = new Map<string, ImportDescription>();
readonly exportDescriptions = new Map<string, ExportDescription>();
readonly dependencies = new Set<Module | ExternalModule>();
isExecuted = false;
hasTreeShakingPassStarted = false;
include(): void {
if (!this.hasTreeShakingPassStarted) return;
// 包含所有副作用
if (this.info.moduleSideEffects) {
this.includeAllExports();
}
// 包含被引用的导入
for (const importDescription of this.importDescriptions.values()) {
const variable = importDescription.module.getVariableForExportName(
importDescription.name
);
if (variable && variable.included) {
this.includeVariable(variable);
}
}
}
}
export interface ImportDescription {
module: Module | ExternalModule;
name: string;
source: string;
start: number;
}
// 示例:import { foo, bar } from './utils'
// importDescriptions:
// Map {
// 'foo' => { module: Module, name: 'foo', source: './utils' },
// 'bar' => { module: Module, name: 'bar', source: './utils' }
// }
// 在 Tree Shaking 时:
for (const [localName, desc] of this.importDescriptions) {
const variable = desc.module.getVariableForExportName(desc.name);
if (variable.included) {
// 只包含被使用的导入
this.includeVariable(variable);
}
}
export interface ExportDescription {
identifier: string | null;
localName: string;
}
// 示例:export const foo = 1; export function bar() {}
// exportDescriptions:
// Map {
// 'foo' => { identifier: 'foo', localName: 'foo' },
// 'bar' => { identifier: 'bar', localName: 'bar' }
// }
getExportNamesByVariable(): Map<Variable, string[]> {
const exportNamesByVariable = new Map<Variable, string[]>();
for (const [exportName, desc] of this.exportDescriptions) {
const variable = this.scope.variables.get(desc.localName);
if (variable) {
const names = exportNamesByVariable.get(variable) || [];
names.push(exportName);
exportNamesByVariable.set(variable, names);
}
}
return exportNamesByVariable;
}
export default class Chunk {
dependencies = new Set<Chunk | ExternalChunk>();
readonly entryModules: Module[] = [];
private readonly exports = new Set<Variable>();
private readonly imports = new Set<Variable>();
generateExports(): void {
// 生成导出映射
if (this.facadeModule !== null) {
const exportNamesByVariable =
this.facadeModule.getExportNamesByVariable();
for (const [variable, exportNames] of exportNamesByVariable) {
this.exportNamesByVariable.set(variable, [...exportNames]);
for (const exportName of exportNames) {
this.exportsByName.set(exportName, variable);
}
}
}
}
}
generateFacades(): Chunk[] {
const facades: Chunk[] = [];
const entryModules = new Set([
...this.entryModules,
...this.implicitEntryModules
]);
for (const module of entryModules) {
// 检查是否需要创建门面 Chunk
if (!this.facadeModule) {
if (this.canModuleBeFacade(module, exposedVariables)) {
this.facadeModule = module;
this.assignFacadeName(...);
}
}
// 创建额外的门面 Chunk
for (const facadeName of requiredFacades) {
facades.push(Chunk.generateFacade(...));
}
}
return facades;
}
export class ExpressionEntity implements WritableEntity {
protected flags = 0;
get included(): boolean {
return isFlagSet(this.flags, Flag.included);
}
set included(value: boolean) {
this.flags = setFlag(this.flags, Flag.included, value);
}
include(context: InclusionContext, _includeChildren: boolean): void {
if (!this.included) this.includeNode(context);
}
includeNode(_context: InclusionContext): void {
this.included = true;
}
// 路径去优化
deoptimizePath(_path: ObjectPath): void {}
// 字面量值获取
getLiteralValueAtPath(_path: ObjectPath): LiteralValueOrUnknown {
return UnknownValue;
}
}
include 是 Tree Shaking 的核心,标记节点是否应该保留
// 示例:函数声明
class FunctionDeclaration extends ExpressionEntity {
include(context: InclusionContext): void {
if (!this.included) {
this.included = true;
// 包含函数体
this.body.include(context, false);
// 包含参数
for (const param of this.params) {
param.include(context, false);
}
}
}
}
// 示例:变量声明
class VariableDeclaration extends ExpressionEntity {
include(context: InclusionContext): void {
if (!this.included) {
this.included = true;
// 包含初始化表达式
if (this.init) {
this.init.include(context, false);
}
}
}
}
deoptimize 用于标记不确定的值,防止过度优化
// 示例:属性访问
class MemberExpression extends ExpressionEntity {
deoptimizePath(path: ObjectPath): void {
// 如果对象是动态的,整个路径都要去优化
if (this.object.included) {
this.object.deoptimizePath([this.property, ...path]);
}
}
}
// 示例:函数调用
class CallExpression extends ExpressionEntity {
deoptimizeArgumentsOnInteractionAtPath(
interaction: NodeInteraction,
path: ObjectPath
): void {
// 去优化所有参数
for (const arg of this.arguments) {
arg.deoptimizePath(UNKNOWN_PATH);
}
// 去优化返回值
this.callee.deoptimizePath(path);
}
}
Rollup 使用多轮迭代确保所有依赖都被正确标记
do {
this.needsTreeshakingPass = false;
for (const module of this.modules) {
if (module.isExecuted) {
module.include();
// 处理新发现的依赖
for (const entity of this.newlyIncludedVariableInits) {
entity.include(createInclusionContext(), false);
}
}
}
} while (this.needsTreeshakingPass);
| 阶段 | 操作 | 数据结构 |
|---|---|---|
| 1. 加载 | ModuleLoader.addEntryModules | Module[] |
| 2. 解析 | parseAsync → AST | ProgramNode |
| 3. 绑定 | bindReferences | Variable, Scope |
| 4. 标记 | includeStatements | included flags |
| 5. 生成 | Chunk.generateExports | OutputChunk |
// ModuleLoader.addEntryModules
async addEntryModules(
unresolvedModules: UnresolvedModule[],
isEntry: boolean
): Promise<{ entryModules: Module[]; implicitEntryModules: Module[] }> {
const entryModules: Module[] = [];
for (const unresolved of unresolvedModules) {
const module = await this.loadModule(unresolved.id);
module.info.isEntry = isEntry;
entryModules.push(module);
// 递归加载依赖
await this.fetchDependencies(module);
}
return { entryModules, implicitEntryModules };
}
// 递归加载依赖
async fetchDependencies(module: Module): Promise<void> {
for (const source of module.sources) {
const dependency = await this.loadModule(source);
module.dependencies.add(dependency);
await this.fetchDependencies(dependency);
}
}
Rollup 使用 Rust 编写的原生解析器提升性能
// 使用 napi-rs 绑定 Rust 解析器
import { parseAsync } from '../native';
// 解析代码
const ast = await parseAsync(code, {
allowReturnOutsideFunction: true,
// ... 其他选项
});
// 转换为 TypeScript AST
const program = convertProgram(ast.buffer);
// AST 节点类型
interface ProgramNode {
type: 'Program';
body: Statement[];
sourceType: 'module' | 'script';
}
class ModuleScope extends ChildScope {
constructor(parent: GlobalScope, context: AstContext) {
super(parent, context);
// 模块级变量
this.variables = new Map<string, Variable>();
}
// 查找变量
findVariable(name: string): Variable {
// 1. 查找本地变量
if (this.variables.has(name)) {
return this.variables.get(name)!;
}
// 2. 查找导入
const importDesc = this.context.importDescriptions.get(name);
if (importDesc) {
return importDesc.module.getVariableForExportName(importDesc.name);
}
// 3. 查找父作用域
return this.parent.findVariable(name);
}
}
class Identifier extends NodeBase {
bind(): void {
// 查找变量定义
this.variable = this.scope.findVariable(this.name);
// 记录引用关系
if (this.variable) {
this.variable.addReference(this);
}
}
include(context: InclusionContext): void {
if (!this.included) {
this.included = true;
// 包含引用的变量
if (this.variable) {
this.variable.include(context, false);
}
}
}
}
副作用(Side Effects)是 Tree Shaking 的难点
// 判断语句是否有副作用
class ExpressionStatement extends NodeBase {
hasEffects(context: HasEffectsContext): boolean {
return this.expression.hasEffects(context);
}
}
// 函数调用有副作用
class CallExpression extends NodeBase {
hasEffects(context: HasEffectsContext): boolean {
return true; // 保守估计
}
}
// 纯函数优化
const pureFunctions = {
Math: {
abs: true,
floor: true,
// ...
}
};
// 如果函数是纯的,可以 Tree Shake
if (pureFunctions[callee.name]) {
return false; // 无副作用
}
class Chunk {
render(
options: NormalizedOutputOptions,
chunkByModule: Map<Module, Chunk>
): { code: string; map: SourceMap } {
const magicString = new MagicStringBundle();
// 1. 生成导入语句
for (const dependency of this.dependencies) {
const importBlock = this.renderImportBlock(dependency);
magicString.addSource(importBlock);
}
// 2. 生成模块代码(只包含 marked 代码)
for (const module of this.orderedModules) {
const moduleCode = module.render(options);
magicString.addSource(moduleCode);
}
// 3. 生成导出语句
const exportBlock = this.renderExportBlock();
magicString.addSource(exportBlock);
return {
code: magicString.toString(),
map: magicString.generateMap()
};
}
}
MagicString 是 Rollup 的高效字符串操作库
import MagicString from 'magic-string';
const code = 'const foo = 1; const bar = 2;';
const s = new MagicString(code);
// 删除未使用的代码
s.remove(16, 30); // 删除 'const bar = 2;'
// 重命名变量
s.overwrite(6, 9, 'baz');
// 生成 source map
const map = s.generateMap({
source: 'source.js',
file: 'bundle.js',
includeContent: true
});
console.log(s.toString()); // 'const baz = 1;'
console.log(map); // SourceMap object
class Graph {
readonly cachedModules = new Map<string, ModuleJSON>();
declare private pluginCache?: Record<string, SerializablePluginCache>;
constructor(options: NormalizedInputOptions) {
if (options.cache !== false) {
// 从缓存恢复模块
if (options.cache?.modules) {
for (const module of options.cache.modules) {
this.cachedModules.set(module.id, module);
}
}
// 从缓存恢复插件状态
this.pluginCache = options.cache?.plugins || Object.create(null);
}
}
getCache(): RollupCache {
return {
modules: this.modules.map(m => m.toJSON()),
plugins: this.pluginCache
};
}
}
class ModuleLoader {
readonly fileOperationQueue: Queue;
constructor(graph: Graph, options: NormalizedInputOptions) {
// 控制并发文件操作
this.fileOperationQueue = new Queue(options.maxParallelFileOps);
}
async loadModule(id: string): Promise<Module> {
return this.fileOperationQueue.run(async () => {
// 并行加载模块
const code = await this.readFile(id);
const ast = await parseAsync(code);
return new Module(graph, id, ast);
});
}
}
// Plugin 并行执行
await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
class Graph {
readonly astLru = flru<ProgramNode>(5); // 5 个最近使用
getAstFromCache(code: string): ProgramNode | null {
const cached = this.astLru.get(code);
if (cached) {
return cached;
}
return null;
}
setAstToCache(code: string, ast: ProgramNode): void {
this.astLru.set(code, ast);
}
}
// 好处:
// 1. 避免重复解析相同代码
// 2. Watch 模式下性能提升明显
// 3. 内存占用可控(固定大小)
class RollupWatcher extends EventEmitter {
constructor(configs: RollupOptions[]) {
super();
this.configs = configs;
this.watcher = new FSWatcher();
// 监听文件变化
this.watcher.on('change', async (id) => {
// 1. 使缓存失效
const module = this.graph.modulesById.get(id);
if (module) {
module.invalidate();
}
// 2. 增量构建
await this.build();
// 3. 触发事件
this.emit('build', result);
});
}
}
class PluginDriver {
constructor(
private graph: Graph,
private options: NormalizedInputOptions,
private plugins: Plugin[],
private pluginCache: Record<string, SerializablePluginCache>
) {}
// 并行执行 hook
async hookParallel<Hook extends string>(
hookName: Hook,
args: any[]
): Promise<void> {
await Promise.all(
this.plugins.map(plugin =>
plugin[hookName]?.apply(plugin, args)
)
);
}
// 串行执行 hook(返回值)
async hookSequential<Hook extends string>(
hookName: Hook,
args: any[]
): Promise<any> {
for (const plugin of this.plugins) {
const result = await plugin[hookName]?.apply(plugin, args);
if (result) return result;
}
}
}
| Hook | 时机 | 用途 |
|---|---|---|
| buildStart | 构建开始 | 初始化资源 |
| resolveId | 解析模块 | 自定义路径解析 |
| load | 加载模块 | 自定义加载逻辑 |
| transform | 转换代码 | 转译、优化 |
| renderChunk | 生成代码 | 代码后处理 |
| writeBundle | 写入完成 | 后续处理 |
// rollup.config.js
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
// Tree Shaking 配置
treeshake: {
moduleSideEffects: false, // 无副作用模块
propertyReadSideEffects: false, // 属性读取无副作用
unknownGlobalSideEffects: false, // 未知全局无副作用
},
// 保留导出签名
preserveEntrySignatures: 'strict',
// 压缩
minifyInternalExports: true,
},
// 缓存
cache: true,
// 并行
maxParallelFileOps: 20,
};
// ✅ 具名导出
export const foo = 1;
export function bar() {}
// ✅ 纯函数
export function add(a, b) {
return a + b;
}
// ✅ 常量
export const MAX_SIZE = 100;
// ❌ 默认导出
export default { foo, bar };
// ❌ 副作用代码
export const data = fetch('/api');
// ❌ 全局变量
window.foo = 1;
// ❌ 原型修改
Array.prototype.foo = 1;
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "dist/my-lib.cjs.js",
"module": "dist/my-lib.esm.js",
"exports": {
".": {
"import": "./dist/my-lib.esm.js",
"require": "./dist/my-lib.cjs.js",
"types": "./dist/my-lib.d.ts"
}
},
"sideEffects": false,
"files": ["dist"]
}
错误 1:过度使用默认导出
// ❌ 无法 Tree Shake
export default {
foo: 1,
bar: 2,
baz: 3
};
// ✅ 可以 Tree Shake
export const foo = 1;
export const bar = 2;
export const baz = 3;
错误 2:修改全局对象
// ❌ 无法 Tree Shake(副作用)
window.myGlobal = {};
Array.prototype.customMethod = () => {};
document.body.innerHTML = '';
// ✅ 隔离副作用
export function init() {
window.myGlobal = {};
}
// 在 package.json 标记副作用
{
"sideEffects": ["**/init.js"]
}
| 特性 | Rollup | Webpack |
|---|---|---|
| Tree Shaking | ✅ 原生支持 | ✅ 需要配置 |
| 输出格式 | ESM 优先 | CommonJS 优先 |
| 代码分割 | 基础 | 强大 |
| HMR | 基础 | 完善 |
| 适用场景 | 库打包 | 应用打包 |
Vite 在生产环境使用 Rollup 进行打包
# 生成可视化分析报告
rollup -c --plugin visualizer
# 输出模块依赖图
rollup -c --plugin dependency-graph
# 查看未使用的导出
rollup -c --plugin analyzer
# 启用详细日志
ROLLUP_LOG_LEVEL=debug rollup -c
# 使用 sourcemap 调试
rollup -c --sourcemap
// ❌ 导入整个库(70KB)
import _ from 'lodash';
_.map([1,2,3], n => n * 2);
// ✅ 按需导入(1KB)
import map from 'lodash/map';
map([1,2,3], n => n * 2);
// ✅ 使用 lodash-es(ESM 版本)
import { map } from 'lodash-es';
map([1,2,3], n => n * 2);
// package.json
{
"sideEffects": false // 告诉打包器无副作用
}
// React 17+ 支持更好的 Tree Shaking
// ❌ 旧写法(无法 Tree Shake)
import React from 'react';
// ✅ 新写法(可以 Tree Shake)
import { useState, useEffect } from 'react';
// ✅ 生产环境自动优化
// React 使用 #__PURE__ 注释标记纯函数
function Component() {
return /*#__PURE__*/ React.createElement('div');
}
// 配置 package.json
{
"sideEffects": false
}
Rollup 4 正在开发中,带来更多优化
| 工具 | 用途 |
|---|---|
| Vite | 开发服务器 + 生产打包 |
| WMR | 轻量级构建工具 |
| Rollup Plugin | 扩展 Rollup 功能 |
| Rollup Starter | 项目模板 |
Q1: 为什么有些代码没有被 Tree Shake?
// A: 检查是否有副作用
// 1. 确保使用 ESM 语法
export const foo = 1; // ✅
// 2. 避免 IIFE
(function() { ... })(); // ❌
// 3. 检查 package.json 的 sideEffects
{
"sideEffects": false // ✅
}
Q2: 如何调试 Tree Shaking?
# 使用 rollup-plugin-visualizer
npm install rollup-plugin-visualizer -D
# rollup.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({
open: true,
filename: 'stats.html'
})
]
};
# 打开 stats.html 查看模块大小
| 模式 | 应用 |
|---|---|
| Visitor | AST 遍历 |
| Observer | Watch 模式 |
| Strategy | Output Format |
| Builder | Chunk 生成 |
| Facade | 门面 Chunk |
| 项目 | 说明 |
|---|---|
| esbuild | 极速打包器(Go) |
| SWC | Rust 编译器 |
| Turbopack | 增量编译(Rust) |
| Bun | JS 运行时 + 打包器 |
Rollup 的 Tree Shaking 是静态分析和迭代标记的完美结合
Rollup Tree Shaking 源码解读
2026-03-14 | 构建工具深度解读