打包 / 构建工具速查(Babel / tsc / Webpack / Vite / Rollup / tsup / Rspack / Turborepo / esbuild)
这类工具经常被混在一起聊,但它们解决的问题不同:有的负责代码转译,有的负责类型检查,有的负责打包,有的负责开发服务器与构建管线,还有的负责Monorepo 任务编排与缓存。
1. 单个工具:作用 & 使用场景
1.1 Babel
- 作用:JS/TS/JSX 的“语法转译器”,把新语法/语法糖转成目标环境可运行的 JS。
- 常见场景
- 业务前端项目:配合打包器处理 JSX、ES6转为ES5(尖头函数转为普通函数)
- 特点/注意点
- 主要做“语法层面”的转换;是否补齐运行时能力(polyfill)需要额外策略
1.2 tsc(TypeScript Compiler)
- 作用:把 TS 转 JS,并提供 TypeScript 类型检查。
- 常见场景
- 项目/库的类型检查(CI 必跑)
- 产出
.d.ts类型声明文件
- 特点/注意点
tsc的“转译”并不等于“打包”(不会做依赖合并、分包等)
1.3 Webpack
- 作用:通用模块打包器(Module Bundler),能把 JS/CSS/图片/字体等纳入依赖图,输出一个或多个 bundle,并支持强大的插件生态。
- 常见场景
- 历史项目/复杂工程:需要非常强的可配置性和生态支持
- 复杂资源处理、定制构建流程、遗留插件链路
- 特点/注意点
- 能力强但配置与心智成本较高;构建速度常需要优化(缓存/并行/拆分等)
1.4 Vite
- 作用:以开发体验为核心的前端构建工具:开发阶段利用原生 ESM 快速启动,生产阶段通常用 Rollup 进行打包。
- 常见场景
- 新业务项目(React/Vue/Svelte 等):追求启动快、HMR 快
- 中小型到大型 Web 应用(结合插件生态)
- 特点/注意点
- “开发快”是其最大优势;生产构建能力依赖其底层打包器与配置
1.5 Rollup
- 作用:更偏向“库(Library)打包”的打包器,擅长产出干净的 ESM/CJS 包、Tree Shaking 效果好。
- 常见场景
- npm 包 / 组件库 / SDK 打包(强调体积与可复用输出)
- 多格式输出(ESM/CJS/UMD)需求
- 特点/注意点
- 做应用也可以,但更典型的定位是“库打包器”,偏底层,定制化强度高也就是配置更复杂。
1.6 esbuild
- 作用:基于 Go 的高性能打包/转译工具,特点是“极快”。
- 常见场景
- 作为构建链路的底层加速器(转译、压缩、依赖预构建)
- 工具脚本、简单打包、开发工具链的一环
- 特点/注意点
- 生态与深度定制能力相对 Webpack/Rollup 较弱(但速度优势明显)
1.7 tsup
- 作用:面向 TypeScript 库的“零配置打包器”(通常基于 esbuild),目标是快速产出 ESM/CJS、声明文件等。
- 常见场景
- 组件库/工具库/SDK:快速打包发布 npm 包
- 特点/注意点
- 上手快、默认合理,封装的比较好;遇到非常复杂的打包需求可能需要转向 Rollup/自定义链路。
1.8 Rspack
- 作用:基于 Rust 的高性能打包器,目标是 尽可能兼容 Webpack 生态与配置,同时显著提升构建速度。
- 常见场景
- Webpack 项目想提速,又不想迁移太多配置/生态
- 大型工程对构建性能敏感
- 特点/注意点
- 兼容性是关键卖点;落地时要关注项目依赖的 loader/plugin 是否完全兼容
1.9 Turborepo(Turbo)
- 作用:Monorepo 的任务编排/缓存系统(不是打包器),解决“多包、多任务”的增量构建与复用。
- 常见场景
- Monorepo:多个 package/app 共享依赖与工具链,需要统一跑 build/test/lint 并缓存
- 特点/注意点
- 关注的是“任务调度与缓存”,底层具体打包/测试仍由各工具完成(如 Vite/Webpack/tsc)
2. 分类:这些工具分别属于哪一类
- 语法转译器(Transpiler):Babel(也常用于处理 JSX/语法降级)
- 类型检查 / TS 编译器:tsc(类型检查、产出 d.ts)
- 应用打包器(App Bundler):Webpack、Rspack、Vite(Webpack 生态/配置兼容)
- 库打包器(Library Bundler):Rollup、tsup(强 tree-shaking、产出干净)
- 高性能打包/转译内核:esbuild(常被上层工具复用)
- 任务编排/增量缓存:Turborepo(面向 Monorepo 工作流)
3. 同类对比(怎么选)
3.1 Webpack vs Rspack(应用打包器)
- 共同点:定位相近,都面向应用构建;loader/plugin 思路类似
- 差异
- Webpack:生态最成熟、兼容最广、定制能力强,但性能压力更常见
- Rspack:目标是兼容 Webpack 同时显著提速,适合“想提速但不想重写配置”的项目
- 选择建议
- 依赖大量历史 loader/plugin:优先 Webpack 或评估 Rspack 兼容性后迁移
- 构建性能是硬指标:优先评估 Rspack
3.2 Vite vs Webpack(构建工具 vs 打包器体系)
- 核心差异:Vite 更强调“开发阶段启动与 HMR 速度”,Webpack 更强调“统一可配置的打包体系”
- 选择建议
- 新项目:通常优先 Vite(开发体验更好)
- 复杂遗留工程/需要深度定制:Webpack(或 Rspack)更稳
3.3 Rollup vs tsup(库打包)
- Rollup:更强可控性、插件生态成熟、产物更可精细雕琢(适合复杂库)
- tsup:上手更快、默认更省心(适合中小型库/工具库快速发布)
3.4 Babel vs tsc(转译与类型检查分工)
- Babel:强在语法转译与生态(JSX/插件体系);不负责“完整类型检查”
- tsc:强在类型系统与 d.ts 产出;“转译”不等于打包
- 常见组合:开发/构建用 Babel 或更快的转译链路;CI/提交前用 tsc 保证类型正确
3.5 esbuild 在体系里的位置
esbuild 常作为“更快的底层引擎”被复用:用于转译、压缩、预构建依赖等;上层仍可能用 Vite/tsup 等提供完整体验与生态。
4. Webpack(专题)
1. Webpack 是什么?解决了什么问题?
一句话:Webpack 是模块打包器(module bundler),把我们写的模块化代码(JS/CSS/图片/字体等)构建成浏览器可加载的静态资源,并在构建过程中完成各种优化(压缩、分包、缓存、按需加载等)。
它主要解决:
- 模块化与依赖管理:从
entry出发构建依赖图,保证依赖加载顺序与可执行性 - 资源统一处理:通过 loader 把非 JS和JSON 资源也变成“可被打包的模块”
- 工程化与优化:开发体验(dev-server、HMR)+ 生产优化(minify、splitChunks、hash、缓存)
2. 核心概念(面试高频)
- Entry:构建的起点
- Module:Webpack 眼里的“一切皆模块”(JS/CSS/图片…)
- Chunk:一组模块的集合,通常对应一次编译/分包结果(懒加载会产生新的 chunk)
- Bundle:最终输出到磁盘的文件(
app.[contenthash].js) - Loader:把 A 类型文件转换成 Webpack 可理解的模块(更像“文件转换器”)
- Plugin:介入整个构建生命周期,做更通用/更强的事情(优化、注入、生成资源等)
3. Webpack 构建流程(从入口到产物)
可以按 6 步复述:
- 初始化:读取并合并配置,创建
Compiler - 编译(解析依赖):从 entry 出发递归解析依赖,交给 loader 处理
- 生成依赖图:模块之间关系确定
- 优化:tree shaking、代码压缩、拆包、hash 等
- 输出:把 chunk 生成最终资源写入
output.path - 完成:触发
done等 hooks,交给插件做收尾(如生成报告)
4. Loader:做“文件转换”的规则系统
4.1 Loader 的作用
Webpack 默认能直接处理 JS/JSON。而像 CSS、图片、字体、TS 等资源,必须先经过 loader 转换,才能被纳入 Webpack 的模块依赖图。
Webpack 对依赖的“理解”来自 JS 的 AST(抽象语法树):
- 从入口文件开始,把 JS 解析成 AST
- 在 AST 里识别
import/require,递归找到依赖 - 最终构建出模块依赖图(dependency graph)
问题在于:index.css 不是 JS,Webpack 无法用 JS 解析器把它变成 AST,也就无法识别它的依赖并加入依赖图。
loader 的价值就在这里:把“非 JS 资源”转换成 可被 import 的 JS 模块(例如导出一个样式注入逻辑或资源 URL),从而让它也能进入依赖图参与打包。
4.2 Loader 配置方式
- 配置方式(推荐):在
module.rules里写test/use/include/exclude - 内联方式:在 import 里显式指定(不推荐,难维护)
- CLI 方式:命令行指定(不常用)
注意:loader 支持链式调用,执行顺序从右到左(或从下到上)。
4.3 常见 loader(按资源类型记)
- JS/语法转换:
babel-loader - CSS:
css-loader(解析@import/url())+(开发时)style-loader(注入 style 标签)
生产通常用MiniCssExtractPlugin把 CSS 抽成文件 - 预处理器:
sass-loader/less-loader+postcss-loader - TS:
ts-loader或 “tsc 做类型检查 + babel 做转译”的组合 - 资源文件:Webpack 5 更推荐用
asset modules(替代早期file-loader/url-loader)
5. Plugin:贯穿生命周期的能力扩展
5.1 常见 plugin(记住“解决什么问题”)
- HtmlWebpackPlugin:生成 HTML 并注入资源引用
- MiniCssExtractPlugin:抽离 CSS 为独立文件(利于并行加载与缓存)
- CleanWebpackPlugin:构建前清理输出目录
- DefinePlugin:注入编译期常量(按环境切换逻辑)
- TerserWebpackPlugin:JS 压缩(生产常用)
- CssMinimizerPlugin:CSS 压缩
- BundleAnalyzerPlugin:产物分析(找出大头依赖/重复依赖)
- ImageMinimizerPlugin:图片压缩优化
- CopyWebpackPlugin:复制静态资源到输出目录
6. Loader vs Plugin(一句话区分)
- Loader:面向“某类文件”的转换(把 A → B)
- Plugin:面向“整个构建过程”的扩展(在 hooks 上做事)
7. dev-server 与 HMR(开发体验)
7.1 dev-server 做了什么
- 浏览器请求 dev-server
- dev-server 在内存里提供打包结果(不落盘)
- 源码变化触发重新编译,然后通知浏览器更新
7.2 HMR 原理
- 客户端与 dev-server 建立连接(通常是 WebSocket)
- 编译完成后,server 通知客户端“哪些模块变了”
- 客户端拉取更新模块,按 HMR runtime 的规则进行替换
- JS 模块是否能“无刷新更新”,取决于是否有对应的 accept 逻辑/边界(比如框架层热更新支持)
8. 生产环境:Webpack 性能优化
8.1 优化的目标与优先级
- 目标 1:首屏更快:减少首屏必须下载/执行的资源(尤其是 JS)
- 目标 2:缓存更好:代码不变就命中缓存,代码变了只更新必要部分
- 目标 3:运行更稳:避免过度拆包带来的请求/运行时开销
8.2 压缩与减重(JS/CSS/HTML/图片/字体)
- JS 压缩(Terser)
- 常见动作:去注释、压缩、mangle
- 说明:压缩会影响构建速度,可结合并行与缓存
- CSS 压缩(CssMinimizer)
- 常见动作:去空格、合并规则、优化重复声明
- HTML 压缩
- HtmlWebpackPlugin 的
minify(底层常见html-minifier-terser)
- HtmlWebpackPlugin 的
- 图片压缩
- 用 ImageMinimizerPlugin 或在产物链路统一做图片处理(PNG/JPEG/WebP/AVIF 等)
- 字体与静态资源
- 优先 woff2 等更高压缩效率的格式,配合缓存策略
8.3 Tree Shaking(减少“打进包但没用”的代码)
usedExports:标记导出是否被使用,后续交给压缩器消除sideEffects:声明副作用,避免误删;常在package.json配置- CSS Tree Shaking:常见思路是 PurgeCSS(要防止误删:动态 class、运行时拼接 class 等)
8.4 代码分包:按需加载 + splitChunks(首屏更轻、缓存更好)
目标:减少首屏必须下载的 JS,做到“首屏轻、后续按需”。
- 路由懒加载(dynamic import)通常是第一步
splitChunks解决:- 抽离公共依赖、第三方依赖
- 减少重复打包、提升缓存命中
- 不要过度分包
- chunk 太碎会增加请求与调度成本,也会增加运行时管理开销
- 要结合 HTTP/2/3、CDN 缓存命中、页面跳转路径综合权衡
补充:你原笔记里常见的“约束点”可以帮助理解 splitChunks 的取舍:
- 新 chunk 是否来自
node_modules或是否会被共享 - 新 chunk 体积是否达到阈值(例如压缩前大于某个大小)
- 按需加载 chunk 的并发请求数上限
- 首屏初始加载的并发请求数上限
8.5 缓存优化:文件指纹(Hash)与稳定 chunk
目标:代码不变就命中缓存,代码变了才更新。
- contenthash:内容变 hash 才变(最适合生产)
- 拆出相对稳定的 chunk(如 vendor/runtime),避免业务小改导致全量失效
8.6 传输压缩:Brotli/gzip(注意:还要配合服务端)
- 这类压缩属于 “传输压缩(Content Encoding)”:资源在离开服务器前被压缩,浏览器收到后再解压。由于传输的数据更小,通常能显著降低下载耗时(尤其弱网/高 RTT 场景)。
- 常见做法(预压缩):构建阶段用
compression-webpack-plugin生成预压缩产物(例如把app.js生成app.js.gz,也可以生成.br)。- 适合压缩的资源:JS/CSS/HTML/JSON/SVG 等文本类资源(压缩率高)
- 不适合或收益很小的资源:图片/视频 等已经高度压缩的二进制资源(再次 gzip/brotli 收益通常不高)
- 浏览器与服务器如何协商(简化流程)
- 浏览器请求
app.js时会携带Accept-Encoding,声明自己支持的解压算法(如gzip、br等) - 服务端/网关/CDN 检测到支持的编码并命中预压缩资源后:
- 返回压缩后的内容(例如实际返回的是
app.js.br或app.js.gz的内容) - 在响应头标记
Content-Encoding: br或Content-Encoding: gzip,告诉浏览器“需要先解压再交给 JS/CSS 解析器”
- 返回压缩后的内容(例如实际返回的是
- 浏览器自动解压并继续解析/执行(解压开销通常远小于网络传输节省的时间)
- 浏览器请求
- 关键点(决定是否真的生效)
- 最终是否返回
.br/.gz,取决于服务端/网关/CDN 是否正确支持并配置了对应的协商与缓存头(不仅仅是你生成了文件) - 一般 Brotli(
br)可以理解为 gzip 的“更强版本”,在 Web 文本资源上常见压缩率更好
- 最终是否返回
补充:
npm pack生成的.tgz是 发布/分发包的压缩格式,本质上是 “先 tar 打包成一个归档,再 gzip 压缩”(tar + gzip)。它解决的是“把多个文件打成一个包方便分发”,与 Web 的Content-Encoding(传输压缩与解压)属于不同层面的概念。
8.7 “内联小 chunk”
- 目的:把体积很小但首屏必需的 runtime 等内联进 HTML,减少一次请求
- 取舍:是否收益要结合 HTTP/2/3、缓存命中与 HTML 缓存策略综合评估
8.8 产物分析:先找到“大头”(最重要的闭环)
- Bundle 分析:最大依赖、重复依赖、某页面把不该进首屏的库带进来了
- Coverage:首屏加载了多少“没用到”的代码
- 常见动作:把大依赖从首屏移走、拆成按需加载、替换更轻的库
9. 提升构建速度(开发/CI)
常见手段(按收益排序):
- 缩小编译范围:
include/exclude精准匹配 loader - 缓存:Webpack 5 filesystem cache(增量构建收益大)
- 多进程/并行:压缩等重任务并行(如 terser 并行)
- 合理的 sourcemap:开发与生产策略不同(越细越慢)
- resolve 优化
resolve.extensions不要乱堆后缀(会增加尝试次数)resolve.alias给常用路径起别名减少查找
10. 开发环境 vs 生产环境(怎么讲更像“工程实践”)
- 开发环境目标:快、可调试(HMR、较友好的 sourcemap、缓存、范围控制)
- 生产环境目标:小、稳、可缓存(压缩、分包、hash、抽离 CSS、优化资源)
11. 常见误区(写在最后更有用)
- “为了减少请求把所有东西打成一个大包”:首屏会更慢,缓存也更差
- “过度分包”:chunk 太碎导致请求过多/缓存命中差/运行时开销变大
- “只关注 JS 体积”:CSS、图片、字体、HTML、第三方脚本同样影响首屏
12. 补充
12.1 常见 plugin 清单(带用途)
- HtmlWebpackPlugin:生成 HTML 并注入资源引用(生产可配合
minify压缩) - MiniCssExtractPlugin:抽离 CSS 为独立文件(利于并行加载与缓存)
- CleanWebpackPlugin:构建前清理输出目录
- TerserWebpackPlugin:压缩 JS(去注释/压缩/mangle)
- CssMinimizerPlugin:压缩 CSS
- DefinePlugin:注入编译期常量
- CopyWebpackPlugin:复制静态文件到输出目录
- HotModuleReplacementPlugin:开启 HMR(通常由 dev-server/框架工具链集成)
- ImageMinimizerPlugin:图片压缩优化
- BundleAnalyzerPlugin:可视化产物组成(找出体积大头/重复依赖)
12.2 构建速度优化
- 升级版本(工具链升级常带来增量编译/缓存优化)
- loader 规则用
include/exclude/test缩小范围 resolve.extensions不要乱加(会增加解析尝试次数)resolve.alias给常用路径起别名- 合理 sourcemap 策略(越精细越慢)
- 并行压缩/多进程(重任务并行)
- 历史方案:DLLPlugin / cache-loader 等(新项目更建议优先用 Webpack 5 内置缓存能力)
13. 拓展:Metro(React Native Bundler)与 Webpack/Vite 的区别
这部分用于帮助你在“打包工具对比”类问题里把 Web(Webpack/Vite) 和 React Native(Metro) 的差异讲清楚:它们都叫 bundler,但运行环境和产物形态不同。
13.1 Metro 是什么?
Metro 是 Meta (Facebook) 为 React Native 提供的 JavaScript 打包器(Bundler),开发阶段通常作为“打包服务器”运行。
可以用 3 个阶段理解它的工作:
- Resolution(解析):解析
import,找到模块文件 - Transformation(转换):用 Babel 等把 JSX/TS/新语法转换成可运行的 JS
- Serialization(序列化):把模块序列化为 bundle(或特定格式)供 App 加载
13.2 为什么 RN 不直接用 Webpack?
核心原因是 Web 和 Mobile 的差异:
- 平台文件解析(
.ios.js/.android.js)- Metro 内置平台后缀解析:同一个 import 在不同平台会命中不同文件
- Webpack/Vite 通常不会默认处理这种平台后缀(需要额外约定/插件)
- 开发体验目标不同
- Metro 的核心目标之一是支持 RN 开发的快速迭代(Fast Refresh)与大规模代码库的增量速度
- 产物与运行时不同
- Web:产物常是 HTML/CSS/JS,浏览器下载后解析执行
- RN:产物以 JS bundle 为主;配合 Hermes 等引擎时会走更偏“预编译/字节码化”的链路(降低运行时解析成本)
- 按需加载模型不同
- Web:常见
import()→ 网络加载 chunk - RN:可以采用 RAM Bundles 等思路:通过索引在本地从大包里快速定位并加载模块(减少“再发一个网络请求”)
- Web:常见
13.3 Metro 是否需要 gzip/brotli?
通常不把 Metro 理解成“像 Web 那样做传输层 gzip/br 的工具”:
- Web:静态资源通过网络分发,经常依赖
Content-Encoding: br/gzip - RN:bundle 通常随
.apk/.aab/.ipa分发,安装包本身就是压缩容器;热更新(如 CodePush)也多以“更新包压缩”来分发
13.4 补充:Metro 打包服务器(Dev Server / Bundler Server)在干什么?
开发环境下,Metro 会起一个本地服务(常见端口 8081),App(模拟器/真机)会向它请求 bundle 与调试信息(如 sourcemap)。
你可以把它理解为“给 RN App 提供 bundle 的开发服务器”,它的关键点是:
- 按需构建与缓存:请求到来时做解析/转换/序列化,并尽可能复用缓存实现增量
- 刷新链路:文件变更后触发增量处理,并配合 Fast Refresh 让 App 更新
- 真机联调依赖网络可达:真机必须能访问到开发机的 Metro Server(同网段、端口可达、注意防火墙/代理)