打包 / 构建(Babel / tsc / Webpack / Vite / Rollup / tsup / Rspack / Turborepo ...)

3 阅读16分钟

打包 / 构建工具速查(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
  • CSScss-loader(解析 @import/url())+(开发时)style-loader(注入 style 标签)
    生产通常用 MiniCssExtractPlugin 把 CSS 抽成文件
  • 预处理器sass-loader / less-loader + postcss-loader
  • TSts-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
  • 图片压缩
    • 用 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,声明自己支持的解压算法(如 gzipbr 等)
    • 服务端/网关/CDN 检测到支持的编码并命中预压缩资源后:
      • 返回压缩后的内容(例如实际返回的是 app.js.brapp.js.gz 的内容)
      • 在响应头标记 Content-Encoding: brContent-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 等思路:通过索引在本地从大包里快速定位并加载模块(减少“再发一个网络请求”)

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(同网段、端口可达、注意防火墙/代理)