普通视图

发现新文章,点击刷新页面。
昨天以前首页

深入浅出 Tree Shaking:Rollup 是如何“摇”掉死代码的?

2026年4月18日 18:35

前言

在前端性能优化中,减小 JS 包体积是重中之重。Tree Shaking(摇树优化) 就像它的名字一样:通过摇晃代码这棵大树,让那些无用的“枯叶”(死代码)掉落。本文将带你揭秘 Rollup 实现 Tree Shaking 的底层原理。

一、 核心基石:为什么是基于ESM?

Tree Shaking 的实现并非偶然,它深度依赖于 ESM (ES Module) 规范。

  • 静态分析:ESM 要求 importexport 必须在代码顶层,不能出现在 if 块或函数内部。
  • 编译时确定:这意味着 Rollup 不需要执行代码,只需扫描一遍源码,就能在编译阶段清晰地知道模块间的依赖关系。
  • 对比 CommonJSrequire 是动态加载的,只有运行到那一行才知道加载了什么,因此 CJS 无法进行彻底的 Tree Shaking。

二、Rollup Tree Shaking 实现原理:从扫描到删除的四步曲

Rollup 的“摇树”过程可以分为以下四个精密步骤:

1. 递归扫描与依赖图构建

从入口文件(如 main.js)开始,递归扫描所有 import/export 语句。Rollup 会记录:

  • 每个模块导出了哪些变量/函数。
  • 每个模块导入了哪些内容。
  • 模块间的引用链路(A 引用了 B 的哪个具体成员)。
  • 基于这些信息,Rollup 会构建出一个完整的模块依赖图,清晰呈现整个项目的代码引用链路。

步骤 2:标记活代码与死代码

在模块依赖图的基础上,Rollup 会从入口文件出发,反向追踪所有被引用的内容,标记出活代码和死代码:

  • 首先标记出哪些导出项被外部(其他模块或入口文件)引用;
  • 接着判断这些被引用的导出项,是否真的在代码中被使用(而非仅导入未使用),若被使用则标记为活代码,未被使用则标记为死代码

步骤 3:AST 分析优化(补充细节)

在标记活代码的过程中,Rollup 会深入分析每个模块的 AST(抽象语法树) ,精准追踪变量、函数的定义和引用关系。这里有一个容易被忽略的点:

  • 即使一个变量、函数在模块内定义了,但既没有被 export 导出,也没有在模块内部被引用,它依然会被判定为死代码,被 Tree Shaking 摇掉——也就是说,Tree Shaking 不仅会处理“导出未使用”的代码,也会清理模块内部“定义未使用”的冗余代码。

步骤 4:删除死代码,生成最终产物

最后,Rollup 会遍历所有模块,只保留标记为活代码的内容,直接删除所有死代码(未被引用的导出项、模块内部未使用的定义等),最终生成精简、无冗余的打包产物。

Rollup 的 Tree Shaking 是原生支持的,无需额外配置,打包时会自动执行上述流程,且输出的代码更接近手写风格,无多余的运行时代码,优化效果直观可见。


三、 实战:不同导出方式的“招魂”效果

Tree Shaking 的效果,很大程度上取决于代码的导出方式——只有静态导出才能被 Rollup 精准分析,动态导出则无法实现 Tree Shaking。以下是常见的导出方式对比:

导出方式 是否支持树摇 深度原因分析
export const a = 1 完美支持 静态导出,引用关系明确。
export function b() {} 完美支持 未被调用时可被精准识别并删除。
export default { a:1 } 不支持/效果差 默认导出是一个对象,工具难以判断你是否会动态访问对象的某个 Key。
export * from './x.js' 支持 按需转发,只会转发那些被下游真正引用的成员。

四、 Tree Shaking 避坑指南

避坑点 1:CommonJS 模块会导致 Tree Shaking 罢工

  • 如果项目中引入了使用 require/module.exports 的第三方库,Tree Shaking 会直接失效。原因如下:

    • CommonJS 模块是动态模块,require 可以接收变量(如 require(./${name}.js)),导入导出关系只能在运行时确定,Rollup 无法在打包前进行静态分析,因此无法识别死代码,Tree Shaking 自然无法生效。

    实战建议:优先使用支持 ESM 规范的第三方库(如 lodash-es 替代 lodash),避免在 ESM 项目中混用 CommonJS 模块。

避坑点 2:副作用代码会干扰 Tree Shaking

  • 如果模块中存在“副作用代码”(即执行后会影响全局环境、修改外部变量、执行 DOM 操作等的代码,如顶层的 console.log、window.xxx = xxx),即使这些代码未被引用,Rollup 也会保守地保留它们,避免影响项目运行逻辑,从而导致部分死代码无法被摇掉。

    解决方案:如果确认模块无副作用,可在 package.json 中添加 "sideEffects": false,告诉 Rollup 该模块可安全删除未引用代码;若有部分文件有副作用(如 CSS 文件),可显式声明:"sideEffects": ["./src/style.css"]

避坑点 3:动态访问会导致 Tree Shaking 失效

  • 如果代码中存在动态访问导出项的情况(如 import * as utils from './utils.js'; utils[dynamicKey]()),Rollup 无法在静态分析阶段确定哪些导出项被使用,会保留整个模块的所有导出项,导致 Tree Shaking 失效。

    实战建议:尽量使用具名导入(import { func } from './utils.js'),避免动态访问导出项。


五、 总结

Rollup Tree Shaking 的核心是“基于 ESM 静态规范,通过静态分析识别并删除死代码”,其流程简洁高效,且原生支持无需额外配置。想要用好 Tree Shaking,关键记住 3 点:

  • 坚持使用 ESM 规范(import/export),避免混用 CommonJS;
  • 优先使用静态具名导出,避免默认导出对象、动态导出;
  • 处理好副作用代码,必要时通过 package.json 的 sideEffects 字段声明。

深度起底 Vite:从打包流程到插件钩子执行时序的全链路解析

2026年4月18日 15:49

前言

Vite 之所以能颠覆 Webpack 的统治地位,不仅是因为它在开发阶段的“快”,更在于它巧妙地结合了 原生 ESMRollup 的生产构建能力。本文将带你拆解 Vite 打包的每一个步骤,并揭秘其插件系统的核心钩子。

一、 Vite 生产打包流水线

Vite 的生产构建完全基于 Rollup 实现,但在 Rollup 打包前后增加了 Vite 特有的预处理和后优化步骤,整个流程可以分为以下 6 个核心阶段:

步骤 1:加载并解析配置

Vite 会优先读取项目根目录的vite.config.js/ts(或vite.config.mjs)配置文件,同时合并命令行参数和默认配置,形成最终的运行配置。

  • 解析核心配置项:root(项目根目录)、base(公共基础路径)、build(打包配置,如输出目录、目标环境、代码分割等)、plugins(插件)、resolve(路径解析)等
  • 同时会读取package.json中的type: module等配置,确定项目的模块规范

配置合并优先级为:命令行参数 > 配置文件导出的配置 > Vite 内置默认配置

步骤 2:预构建与依赖分析

这是 Vite 区别于传统打包工具的关键步骤,主要为了解决第三方依赖的兼容性和打包性能问题。

  • Vite 会扫描项目中的所有依赖(主要是node_modules中的第三方包),对非 ES 模块的依赖(如 CommonJS、UMD 格式)进行预构建,统一转换为标准 ES 模块
  • 分析项目入口文件(默认是根目录的index.html),递归解析所有文件的依赖关系(包括.vue/.js/.ts/.css/.scss等各种类型的文件),构建完整的模块依赖图

补充:预构建的结果会被缓存到node_modules/.vite目录,只有当依赖发生变化时才会重新预构建,极大提升了后续打包速度

步骤 3:插件执行

Vite 会按照配置的顺序依次执行所有插件,并调用相应的插件生命周期钩子,在这个阶段完成各种文件转换、代码预处理和资源处理操作。

  • 插件会在不同的生命周期钩子中执行对应的逻辑,例如:

    • .vue/.jsx/.tsx等非原生 JS 文件进行编译转换
    • 小图片 / 字体等静态资源的 base64 转换(由 Vite 内置的 assets 插件处理)
    • CSS 预处理器编译、PostCSS 处理、CSS Modules 转换等

步骤 4:使用 Rollup 进行核心打包

这是生产构建的核心阶段,Vite 将完全委托给 Rollup 进行代码打包和优化。

  • 按入口文件拆分代码块(chunk),同时支持根据动态导入import()自动拆分代码块,还可以通过配置将第三方依赖单独拆分为 vendor chunk
  • 生成兼容目标环境的代码:默认打包为现代浏览器支持的 ES 模块格式,也可通过build.target配置兼容 ES5 及更低版本
  • 处理模块间的依赖引用:将代码中的相对路径替换为配置的base路径,确保生产环境下所有资源都能正确加载
  • 执行 Rollup 特有的优化:包括 Tree Shaking 移除无用代码、作用域提升(Scope Hoisting)减少代码体积等

步骤 5:Rollup 产物最终优化

在 Rollup 完成基础打包后,Vite 会对输出的产物进行最后一轮的生产环境优化。

  • 对 JavaScript 和 CSS 代码进行压缩混淆(默认使用 Terser 压缩 JS,esbuild 压缩 CSS)
  • 生成资源哈希文件名,实现静态资源的长效缓存
  • 可选生成 sourcemap 文件,方便生产环境调试
  • 可选生成manifest.json文件,记录资源文件名与哈希后的文件名的映射关系,用于后端集成

步骤 6:输出最终产物

将所有打包和优化后的产物输出到指定的目录(默认是dist目录)。

  • 静态资源(JS、CSS、图片、字体等)会输出到dist/assets目录
  • 入口 HTML 文件会输出到dist根目录
  • 其他配置的静态资源会按照原目录结构输出到dist目录

二、 揭秘 Vite 插件钩子 (Hooks)

Vite 的插件系统扩展自 Rollup 的插件系统,同时增加了一些 Vite 特有的钩子函数,以支持开发服务器和热更新等 Vite 独有的功能。

2.1 通用 Rollup 兼容钩子

Vite 在开发阶段会模拟 Rollup 的行为,调用一系列与 Rollup 兼容的钩子;而在生产环境下,由于 Vite 直接使用 Rollup 进行打包,所有 Rollup 插件钩子都会生效。

这些通用钩子主要分为三个阶段:

  1. 服务器启动阶段optionsbuildStart钩子会在开发服务启动时被调用
  2. 请求响应阶段:当浏览器发起请求时,Vite 内部依次调用resolveIdloadtransform钩子
  3. 服务器关闭阶段:Vite 会依次执行buildEndcloseBundle钩子

重要说明:除了以上钩子,其他 Rollup 插件钩子(如moduleParsedrenderChunk等)均不会在 Vite 开发阶段调用。这是因为开发阶段 Vite 采用按需编译的模式,不需要对整个项目进行完整打包。

2.2 Vite 独有钩子

Vite 提供了一些独有的钩子函数,这些钩子只会在 Vite 内部调用,放到纯 Rollup 环境中会被直接忽略。

钩子名称 调用时机 主要用途
config Vite 读取完配置文件之后,最终配置合并之前 对用户导出的配置对象进行自定义修改,例如注入默认值、根据环境动态调整配置、合并插件配置等
configResolved Vite 完成最终配置解析之后 此时配置已完全合并且不可再修改,常用于获取最终配置进行调试,或根据最终配置初始化插件内部状态
configureServer 仅在开发服务器启动时调用 扩展或拦截开发服务器行为,例如添加自定义中间件、模拟 API 接口、修改服务器配置、实现请求代理等
transformIndexHtml 转换原始 HTML 文件内容时调用 动态修改 index.html 内容,例如注入脚本标签、修改 meta 标签、添加全局变量、SSR 内容注入等
handleHotUpdate Vite 服务端处理热更新时调用 自定义热更新逻辑,例如过滤不需要热更新的模块、触发自定义的热更新行为、向客户端发送自定义消息等

三、 核心:钩子函数的执行顺序

理解执行顺序是编写高质量插件的前提。

1. 服务启动阶段

configconfigResolvedoptionsconfigureServerbuildStart

2. 请求响应阶段

  • HTML 请求:仅执行 transformIndexHtml
  • 非 HTML 请求 (JS/CSS等):resolveIdloadtransform

3. 热更新与关闭阶段

  • HMR 触发handleHotUpdate
  • 服务关闭buildEndcloseBundle


四、 知识扩展:开发与生产的差异

  • 开发阶段:Vite 利用浏览器原生 ESM,只有在文件被请求时才触发 transform 钩子,这是其“快”的底层逻辑。
  • 生产阶段:Vite 完完全全变成了一个“Rollup 配置封装器”,所有插件钩子都会遵循 Rollup 的打包逻辑执行。
❌
❌