Turbopack — 把 bundler 重做成增量计算应用
是什么
Turbopack 是 Webpack 作者 Tobias Koppers 在 Vercel 做的第二代 bundler,核心特点是底层有一个叫 Turbo Tasks 的增量计算引擎。日常类比:像一个带记账本的厨师——做过的菜如果食材一模一样,他就把上次的端出来;只有真正变了的那道才会重新烧。
普通 Webpack 每次改一个文件,都要重新解析、重新打包一大圈;Turbopack 把每个步骤(resolve、parse、收集依赖、切 chunk)都包成一个可缓存的任务节点,输入哈希命中就跳过执行。
# 在 Next.js 14+ 项目里直接开(默认已经是 turbopack)next dev --turbo# 改一行 .tsx,浏览器 100-300 ms 就刷新;webpack 通常 1-3 秒它定位是 Next.js 的自家 dev 引擎,而不是 webpack 的通用替代——这点和路线相反的 rspack 形成对照。
为什么重要
不理解 Turbopack 的设计选择,下面这些事都没法解释:
- 为什么同一作者 8 年内做了两遍 bundler——不是 webpack 不够好,是物理上改不快了
- 为什么它故意不兼容 webpack plugin——这是设计选择,不是工期没到
- 为什么 dev 改一个文件能在 100-300 ms 内 hot-reload,而 production build 还在 alpha
- 为什么 2024 年仓库从
turborepo搬进next.js——本质是定位收敛,不再装作通用工具
核心要点
Turbopack 的设计可以拆成 三件事:
-
bundler 是一个增量计算应用:每次文件改动只重做受影响的子图,这不是优化,是第一性原理。学术界 adapton / salsa-adapton 早就给过这种 query-based 增量计算的现成模型。
-
任务节点 = 函数 + 输入哈希:在 Rust 函数上加一个
#[turbo_tasks::function]宏,函数体被搬到调度器里执行。调用时把参数打包算哈希,命中缓存就直接拿旧结果,根本不调用函数。所以函数体里不能有副作用——任何 IO 都得再包一层。 -
可序列化输入还能落盘:能 bincode 编码的输入,引擎会把整张任务图持久化到
.next/cache/turbopack/。重启 dev server 还能命中昨天的缓存——这是它在大型 monorepo 上能”秒启动”的根因。
类比:每个任务节点像一张写着”输入 → 输出”的便利贴,整个项目就是一面贴满便利贴的墙。改一个文件,就把指向它的那些便利贴撕掉重写;其他不动的便利贴继续用,绝大多数情况下”改一行只重算几张便利贴”。
这套设计里最反直觉的一条:函数体里不能有任何副作用——写文件、改全局变量都不行,否则缓存命中就漏掉了这些动作。所有真实 IO 都得再被包成一个独立任务节点,由引擎统一调度。
实践案例
案例 1:Next.js 项目里直接体验 dev 速度
npx create-next-app@latest tp-testcd tp-testnpm run dev -- --turbo# 改 app/page.tsx 任意一行,看终端 "Compiled in 120ms"第一次启动相对慢(任务图要建起来),但第二次启动会从 .next/cache/turbopack/ 直接 hydrate——这就是持久化缓存在生效。同一个项目下,删掉这个目录再跑一次,体感会回到 webpack 那种”几秒才能就绪”的速度,对比非常直观。
案例 2:用 trace 看缓存命中
next dev --turbo --trace .turbo-trace.log# 改完文件后停掉 dev serverturbopack-trace-server .turbo-trace.log# 浏览器打开默认地址,看任务图、命中率、耗时分布trace UI 会列出每个任务节点是 cold(首次执行)/ hit(缓存命中)/ recompute(输入变了),能直观看出哪些步骤拖慢了开发。
比如改一个 CSS 文件,绝大部分 ecmascript 任务应该是 hit;如果发现一片红色 recompute,说明依赖图设计上有不该有的连边——通常是 layout / barrel file 之类把所有 page 串起来的”全局节点”在作怪。
案例 3:把任务图思路用在自己的小 builder 上
不需要真用 Turbopack,也可以借鉴它的设计:
// build.mjs —— 一个迷你静态站点 builderimport { createHash } from 'node:crypto';const cache = new Map();function memo(fn) { return (...args) => { const key = createHash('sha1').update(JSON.stringify(args)).digest('hex'); if (cache.has(key)) return cache.get(key); const v = fn(...args); cache.set(key, v); return v; };}// 把 mtime 也放进 key 里,文件改了哈希自然不一样const renderPage = memo((mdPath, mtime) => /* 真实渲染 */);核心收获不是 Turbopack 本身,是把每一步包成纯函数 + 输入哈希做缓存这套范式——一旦每一步都是纯函数,整个 build 就自然变成”只重算受影响子图”。
踩过的坑
-
Webpack plugin 完全不兼容:要扩展只能写 SWC plugin(Wasm 沙盒)或 ecmascript-plugin(Rust 静态链接);社区可用 plugin 数量目前是 webpack 的 1% 量级,老项目里大量 webpack-specific plugin 不要硬上,先评估迁移成本。
-
production build 截至 2026-05 仍是 alpha:Vercel 自己 next.js 站点 prod 也跑 webpack,多次”下半年 GA”承诺都跳票;prod 路径相比 webpack 仍有大量 edge case,上线前要 staging 充分压测。
-
持久化缓存目录会膨胀:
.next/cache/turbopack/在中型项目能涨到 1-3 GB;CI 节点 cache 策略要重新设计,不能简单复用 webpack 的 cache key,否则缓存命中率会非常糟糕。 -
不适合做库打包:架构假设 entry 是应用 + chunk graph 输出;做需要 single-file ESM/CJS 双产物的库时输出不可控,应该用 rolldown 或 esbuild。
适用 vs 不适用场景
适用:
- Next.js 项目(13+ 默认 dev 引擎,零配置)
- 大型 monorepo / 长期 dev server——持久化缓存优势最明显
- 想把”增量计算”心智用到自己工具链里——Turbopack 是一个活的参考实现
- 团队对 dev 体验敏感,能接受 prod 还沿用 webpack 的双轨过渡期
不适用:
- 已经深度依赖 webpack plugin 生态——选 rspack(专为 webpack 兼容而生)
- 写库 / 工具——选 rolldown 或 esbuild,输出更可控
- 现在就要切 production build——再等 GA,目前仍 alpha
- 完全脱离 Next.js 的通用 bundler 需求——
turbopack-cli是 experimental,不是给生产用的
历史小故事(可跳过)
- 2014 年:Tobias Koppers 发布 webpack 1.0——一份 JS 写的、靠 plugin 串起来的 bundler,奠定了”loader + plugin + chunk”的心智模型。
- 2020 年:Webpack 5 发布,加了 filesystem cache。Tobias 自己最清楚——JS 单线程、没 first-class 缓存抽象,cold build 物理上改不快了。
- 2022-10:Tobias 加入 Vercel 后,在 Next.js 13 发布会推出 Turbopack,宣称”比 vite 快 700ד——后来这个数字被各种场景测试打脸,Vercel 也下调了说法。
- 2023-2025:production GA 多次跳票;社区抱怨 plugin 生态空。
- 2024-08:仓库从独立的
vercel/turborepo搬进vercel/next.js,标志定位收敛——它就是 Next.js 自家 bundler,不再装作通用工具。 - 2026-05:
next dev --turbo在 Next.js 14+ 已是默认;prod build 仍 alpha,社区把它和 rspack / rolldown 放在一起讨论 Rust bundler 三大流派。
学到什么
- 同一个人愿意做两遍 bundler,第一性原理换了:webpack 的心智不动只是实现慢——这是 rspack;bundler 应该是增量计算应用——这是 Turbopack。
- 不兼容是设计选择:Tobias 主动放弃自己创造的 webpack plugin API,是因为它已经成了历史负担——重做就重做彻底,长痛不如短痛。
- 学术理论 → 工业落地需要十年:adapton (2014) → salsa-adapton (2018) → Turbo Tasks (2022),增量计算从论文走到 bundler 心脏花了八年。
- 定位收敛胜于强行通用:从”webpack 接班人”到”Next.js 自家 dev 引擎”,承认边界反而让产品交付质量更高,营销话术更克制。
延伸阅读
- 官方站点:turbo.build/pack(架构图 + benchmark 数据 + 路线图)
- 引擎源码起点:
turbopack/crates/turbo-tasks(看manager.rs怎么调度任务节点) - Next.js 官方博客 turbopack 标签下的几篇 launch / progress 文章——非技术细节但讲清楚定位演变
- Tobias 演讲:搜 “Turbopack: An Incremental Bundler” YouTube,讲为什么 bundler 是 incremental computation
- salsa-adapton —— Turbo Tasks 思想直接借鉴的论文系
- adapton —— 增量计算的更老的根
- rspack —— 路线相反的 webpack 兼容版 Rust bundler,做最强对照
关联
- salsa-adapton —— Turbo Tasks 引擎是 Salsa 思路的工业落地
- adapton —— 更早的增量计算论文,给了”自适应计算”原型
- rspack —— 同样 Rust 写的 bundler,但走 webpack 兼容路线(强对照组)
- rolldown —— Vite 团队的 Rust bundler,更适合做库
- vite —— dev 极致快但 build 走 rollup 的两段式方案
- esbuild —— Go 写的 bundler,奠定”原生语言重写”先例
- swc —— Turbopack 的 ecmascript 后端用的解析器(同源 Vercel 团队)
- next-js —— Turbopack 的最大宿主,定位收敛后两者深度耦合
反向链接
- esbuild —— esbuild — 用 Go 写的极速 JS bundler
- next-js —— Next.js — React 全栈框架
- rolldown —— rolldown — 用 Rust 给 Vite 当统一引擎的打包器
- rspack —— rspack — 用 Rust 重写 webpack 的内核,但留下整个 plugin 生态
- salsa-adapton —— Salsa / Adapton — 让程序只重算”真的变了”的那一小块
- swc —— SWC — Rust 写的 TS/JS 编译器
- turborepo —— Turborepo — 让 monorepo 学会”哪些活已经干过了不要再干”
- vite —— Vite — 浏览器自己加载源码的构建工具
- webpack —— webpack 模块打包
- why-did-you-render —— why-did-you-render — 让 React 告诉你这次渲染到底为什么