Tree Shaking 与动态 import 分包

本节目标

Tree Shaking:打包器分析「哪些导出真的被 import 了」,没用到的整段删除,缩小体积。

// 运行环境:Node.js 16+(演示 import 拆分,实际 Tree Shaking 看打包结果)
// utils.mjs
export const used = () => '用了'
export const unused = () => '没用'   // 如果没人 import unused,打包会被删掉

// 导入时只取 used,unused 会被 Tree Shaking 移除(前提是 ESM 静态、无副作用)
import { used } from './utils.mjs'
console.log(used())

// 动态 import:按需加载,生成单独 chunk(分包)
// 适合「点击后才需要的重型模块」
async function onClick() {
  const heavy = await import('./heavy.mjs') // 运行时才加载这个文件
  heavy.run()
}

会抖不掉的情况:模块顶层有「副作用」(如 console.log、改全局、import './css'),打包器不敢删,怕你本来就要它执行。用 /*#__PURE__*/ 标注纯函数可帮助摇树。

名词解释

Tree Shaking(摇树):打包时把「没被任何地方 import 的导出代码」像摇树掉枯叶一样删掉,减小产物体积。前提是 ESM 静态分析(能确定谁用到谁)。它是前端性能优化的大头——一个库导出 100 个函数你只用一个,摇完可能只剩那一个。

动态 import(Dynamic Import):import('./x.mjs') 写在函数里、返回 Promise,运行时才加载该模块。它让「不常用的模块」不进首屏包,而是「用到时再下载」,即「代码分割 / 按需加载」。和静态 import 不同,它是异步、可条件触发的。

课后练习

练习 1:为什么 Tree Shaking 要求「ESM + 无副作用」?

答案:摇树靠「静态分析确定某导出无人用」然后删除;CJS 的 module.exports 是运行时对象,无法静态判断;若模块顶层有副作用(如连库、打日志),删了会改变程序行为,打包器只好保留。所以「纯 ESM + 纯模块」才摇得干净。

练习 2:动态 import 和静态 import,在产物上有何不同?

答案:静态 import 的模块会被打进「主包」(首屏一起加载);动态 import 的模块会被单独切成一个 chunk,只有执行到那行(如用户点击)才下载。所以动态 import 是「首屏瘦身」的标准手段。

总结

Tree Shaking 和动态 import 是现代前端「体积小、首屏快」的两大支柱。我的观点:写库/写模块时,默认用 ESM 命名导出、保持模块纯净(顶层无副作用),就是给 Tree Shaking 铺路——你不经意写的 console.log 或顶层副作用,可能让整模块抖不掉。动态 import 则要用在「重且不一定用」的地方(图表库、富文本、二级页面),别滥用到首屏核心逻辑上(反而增加请求数)。一句话:摇树管「删没用的」,分包管「晚加载重的」,两者配合,性能才有保障。