其他打包器✅
rollup 为什么快
答案
核心概念 Rollup 之所以在打包速度和产物运行性能上表现出众,核心归功于其面向库(Library)的极简设计哲学与ESM 原生架构:
- ESM 静态分析与天然 Tree-Shaking:
- Rollup 自诞生起便建立在 ES6 Module 规范之上。ESM 的
import和export是完全静态可分析的,Rollup 在构建 AST 阶段即可精准推导符号引用关系,直接剔除无引用的死代码(DCE),无需执行代码。
- Rollup 自诞生起便建立在 ES6 Module 规范之上。ESM 的
- 作用域提升(Scope Hoisting)无运行时胶水代码:
- 传统 Webpack 打包会将每个文件包裹为一个独立的闭包函数并维护一套运行时模块加载器(
__webpack_require__),这带来大量函数调用开销与闭包内存占用。 - Rollup 首创 Scope Hoisting,将所有模块拍平在同一个闭包作用域中,通过严格的变量重命名(Mangle/Renaming)解决命名冲突。最终产物几乎等同于手写的一个干净纯 JS 文件,无任何运行时包裹层(Zero-runtime overhead)。
- 传统 Webpack 打包会将每个文件包裹为一个独立的闭包函数并维护一套运行时模块加载器(
- 精简专注的架构流水线:
- 核心构建流水线直奔主题:
resolveId->load->transform->renderChunk,模块图数据结构轻量,省去了 Webpack 复杂的 ChunkGraph 与多种模块类型(CommonJS/AMD/Assets)的兼容计算。
- 核心构建流水线直奔主题:
- 定位差异对比:
| 维度 | Rollup | Webpack |
|---|---|---|
| 主要定位 | 类库 / SDK / 工具包(Libraries) | 复杂单页应用(Applications) |
| 模块机制 | 原生 ESM 为核心,产物扁平纯粹 | 多种模块规范兼容,依赖运行时加载器 |
| 运行时开销 | 0 运行时胶水代码,执行快、体积小 | 包含 module registry、runtime bundle |
| 特性支持 | 专注于 Tree-Shaking 与作用域提升 | 开箱即用强大 Code Splitting、HMR、资源加载器 |
面试官视角
- 考察点:能否透过“构建快”的表面现象,指出本质是 Scope Hoisting(消除了模块包裹闭包) 与 纯粹 ESM 静态分析(无运行时加载器负担),并能根据“开发组件库选 Rollup/tsdown,开发大型业务系统选 Webpack/Vite”给出合理的架构选型理由。
延伸阅读
rollup 内联优化是什么?
答案
内联优化 是指将模块的代码直接插入到引用它的地方。这种优化可以减少模块的加载时间,提高代码的执行效率。
例如 react 中如下代码 const nextRootContext = getRootHostContext(nextRootInstance); 在打包后会注入 getRootHostContext(nextRootInstance), var nextRootContext = nextRootInstance.nodeType rollup 会在编译的时候识别全局只调用一次的函数,编译的时候直接内联到调用的地方,避免采用引用方式调用的开销
- 打包前
- 打包后
function pushHostContainer(fiber: Fiber, nextRootInstance: Container): void {
// Push current root instance onto the stack;
// This allows us to reset root when portals are popped.
push(rootInstanceStackCursor, nextRootInstance, fiber);
// Track the context and the Fiber that provided it.
// This enables us to pop only Fibers that provide unique contexts.
push(contextFiberStackCursor, fiber, fiber);
// Finally, we need to push the host context to the stack.
// However, we can't just call getRootHostContext() and push it because
// we'd have a different number of entries on the stack depending on
// whether getRootHostContext() throws somewhere in renderer code or not.
// So we push an empty value first. This lets us safely unwind on errors.
push(contextStackCursor, null, fiber);
const nextRootContext = getRootHostContext(nextRootInstance);
// Now that we know this function doesn't throw, replace it.
pop(contextStackCursor, fiber);
push(contextStackCursor, nextRootContext, fiber);
}
function pushHostContainer (fiber, nextRootInstance) {
push(rootInstanceStackCursor, nextRootInstance, fiber)
push(contextFiberStackCursor, fiber, fiber)
push(contextStackCursor, null, fiber)
let nextRootContext = nextRootInstance.nodeType
switch (nextRootContext) {
case 9:
case 11:
nextRootContext = nextRootContext === 9 ? '#document' : '#fragment'
nextRootInstance = (nextRootInstance =
nextRootInstance.documentElement)
? (nextRootInstance = nextRootInstance.namespaceURI)
? getOwnHostContext(nextRootInstance)
: HostContextNamespaceNone
: HostContextNamespaceNone
break
default:
if (
((nextRootInstance =
nextRootContext === 8
? nextRootInstance.parentNode
: nextRootInstance),
(nextRootContext = nextRootInstance.tagName),
(nextRootInstance = nextRootInstance.namespaceURI))
) {
(nextRootInstance = getOwnHostContext(nextRootInstance)),
(nextRootInstance = getChildHostContextProd(
nextRootInstance,
nextRootContext
))
} else {
switch (nextRootContext) {
case 'svg':
nextRootInstance = HostContextNamespaceSvg
break
case 'math':
nextRootInstance = HostContextNamespaceMath
break
default:
nextRootInstance = HostContextNamespaceNone
}
}
}
nextRootContext = nextRootContext.toLowerCase()
nextRootContext = updatedAncestorInfoDev(null, nextRootContext)
nextRootContext = {
context: nextRootInstance,
ancestorInfo: nextRootContext
}
pop(contextStackCursor, fiber)
push(contextStackCursor, nextRootContext, fiber)
}
延伸阅读
- Compiler inlining optimizations 讨论哪些场景会触发内联优化