🦀 Rollup Tree Shaking 实现源码解读

基于 rollup/rollup 深度解析

Graph · Module · Chunk · ExpressionEntity
2026-03-14 | 构建工具深度解读

📋 项目简介

Rollup 是下一代 JavaScript 模块打包器,以 Tree Shaking 著称

  • 核心优势:编译时优化,剔除未使用代码
  • 主要用途:库打包、应用构建
  • 技术栈:TypeScript + Rust(Native Parser)
  • 开源地址:github.com/rollup/rollup
25K+ ⭐ GitHub Stars

🎯 核心概念

概念 说明
Graph 依赖图,管理所有模块和依赖关系
Module 单个模块,包含 AST、导入导出信息
Chunk 代码块,最终输出的代码单元
ExpressionEntity 表达式实体,包含 include/deoptimize 方法

🌳 Tree Shaking 原理

核心思想:通过静态分析标记已使用代码,剔除未使用部分

  • 第一步:构建依赖图(Graph)
  • 第二步:标记入口模块已执行
  • 第三步:迭代标记被引用的代码(include)
  • 第四步:生成 Chunk,剔除未标记代码

📊 Graph 类 - 依赖图管理

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);
      }
    }
  }
}

🔄 Graph.includeStatements - 核心算法

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); // 迭代直到稳定
  }
}

📦 Module 类 - 模块管理

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);
      }
    }
  }
}

📥 Module.importDescriptions - 导入管理

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);
  }
}

📤 Module.exportDescriptions - 导出管理

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;
}

📦 Chunk 类 - 代码块生成

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);
        }
      }
    }
  }
}

🎭 Chunk.generateFacades - 门面模式

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;
}

🔬 ExpressionEntity 类 - 表达式基类

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 方法详解

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 机制

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);
  }
}

🔄 Tree Shaking Pass 迭代

Rollup 使用多轮迭代确保所有依赖都被正确标记

  • Pass 1:标记入口模块的直接依赖
  • Pass 2:包含入口导出(preserveSignature)
  • Pass 3+:处理跨模块副作用和新发现的依赖
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);
  }
}

🌳 AST 解析 - Native Parser

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';
}

🌐 作用域管理 - Scope

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; // 无副作用
}

✂️ Chunk 划分策略

划分依据

  • 动态导入(import())
  • 入口模块
  • 共享依赖
  • manualChunks 配置

优化目标

  • 减少重复代码
  • 并行加载
  • 缓存友好
  • 按需加载

📝 代码生成 - render

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 - 字符串操作

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]);

⚡ 性能优化 - AST LRU 缓存

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. 内存占用可控(固定大小)

👀 Watch 模式 - 增量构建

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);
    });
  }
}

🔌 插件系统 - PluginDriver

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;
    }
  }
}

🪝 常用 Hooks

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;

✅ 最佳实践 - package.json 配置

{
  "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 vs Webpack

特性 Rollup Webpack
Tree Shaking ✅ 原生支持 ✅ 需要配置
输出格式 ESM 优先 CommonJS 优先
代码分割 基础 强大
HMR 基础 完善
适用场景 库打包 应用打包

⚔️ Rollup vs Vite

Vite 在生产环境使用 Rollup 进行打包

  • 开发环境:Vite 使用原生 ESM,无需打包
  • 生产环境:Vite 调用 Rollup 进行 Tree Shaking
  • 配置兼容:Vite 支持大部分 Rollup 配置
  • 插件共享: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

📊 性能 Benchmark

小型项目(100 模块)

  • 构建时间:~200ms
  • 内存占用:~50MB
  • Tree Shaking 效率:95%

大型项目(10000 模块)

  • 构建时间:~5s
  • 内存占用:~500MB
  • Tree Shaking 效率:90%

📦 案例研究 - Lodash Tree Shaking

// ❌ 导入整个库(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 Tree Shaking

// 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 正在开发中,带来更多优化

  • 更快的解析器:继续优化 Rust Native Parser
  • 更好的类型支持:原生支持 TypeScript
  • WASM 支持:浏览器端运行
  • 增量编译:更智能的缓存策略
  • 并行优化:充分利用多核 CPU

🌐 生态系统

工具 用途
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 查看模块大小

🎯 核心算法总结

  1. Graph.build() - 构建依赖图
  2. ModuleLoader - 加载模块
  3. Module.bindReferences() - 绑定引用
  4. Graph.includeStatements() - 标记已使用代码
  5. Module.include() - 递归标记
  6. Chunk.generateExports() - 生成导出
  7. Chunk.render() - 生成代码

🎨 设计模式

模式 应用
Visitor AST 遍历
Observer Watch 模式
Strategy Output Format
Builder Chunk 生成
Facade 门面 Chunk

📚 扩展阅读

🔗 相关项目

项目 说明
esbuild 极速打包器(Go)
SWC Rust 编译器
Turbopack 增量编译(Rust)
Bun JS 运行时 + 打包器

💡 关键要点

核心概念

  • Graph - 依赖图
  • Module - 模块
  • Chunk - 代码块
  • include - 标记算法

最佳实践

  • 使用 ESM
  • 避免副作用
  • 配置 sideEffects
  • 按需导入

🎯 总结

Rollup 的 Tree Shaking 是静态分析迭代标记的完美结合

  • 高性能:Rust Native Parser
  • 精确:多轮迭代确保完整性
  • 灵活:丰富的插件系统
  • 标准:ESM 优先
感谢阅读!🦀

Q&A

欢迎提问和讨论

Rollup Tree Shaking 源码解读
2026-03-14 | 构建工具深度解读