文章目录
-
-
- [1. Webpack](#1. Webpack)
-
- [1.1. Webpack 的五个核心概念](#1.1. Webpack 的五个核心概念)
- [1.2. webpack 编译的全流程](#1.2. webpack 编译的全流程)
-
- [1.2.1. 初始化 (Initialization)](#1.2.1. 初始化 (Initialization))
- [1.2.2. 第二阶段:编译构建 (Compilation) ------ 核心环节](#1.2.2. 第二阶段:编译构建 (Compilation) —— 核心环节)
- [1.2.3. 输出生成 (Seal & Emit)](#1.2.3. 输出生成 (Seal & Emit))
- [1.3. AST 是什么?](#1.3. AST 是什么?)
- [1.4. Webpack 的核心优势](#1.4. Webpack 的核心优势)
- [1.5. plugin 和 loader 的区别](#1.5. plugin 和 loader 的区别)
- [1.6. Webpack 配置成使用 ES Module](#1.6. Webpack 配置成使用 ES Module)
- [2. Vite](#2. Vite)
-
- [2.1. Vite 的核心工作原理](#2.1. Vite 的核心工作原理)
- [2.2. Vite常用配置](#2.2. Vite常用配置)
- [2.3. Vite的代码分割](#2.3. Vite的代码分割)
-
- [底层机制:Rollup 的模块解析与分块算法](#底层机制:Rollup 的模块解析与分块算法)
- 代码分割是否越细越好,判断标准是什么
- [3. Vite vs Webpack](#3. Vite vs Webpack)
-
- [3.1. Vite 热更新(HMR)核心原理](#3.1. Vite 热更新(HMR)核心原理)
- [3.2. Vite 与 Webpack 的 HMR区别](#3.2. Vite 与 Webpack 的 HMR区别)
- [4. ES6 向 ES5 的转换的方式](#4. ES6 向 ES5 的转换的方式)
- [5. 配置优化打包速度或构建速度](#5. 配置优化打包速度或构建速度)
-
- [5.1. webpack优化方案](#5.1. webpack优化方案)
- [5.2. vite优化方案](#5.2. vite优化方案)
-
1. Webpack
1.1. Webpack 的五个核心概念
1.入口 (Entry)
指示 Webpack 应该从哪个文件开始打包。它是依赖图的起点
- 单入口 :
entry: './src/index.js' - 多入口:用于多页面应用(MPA)
2.输出 (Output)
告诉 Webpack 打包后的文件叫什么名字、存在哪个目录下
- 通常配置为
dist目录,文件名常带 Hash (如bundle.[contenthash].js)以解决浏览器缓存问题
3.Loader(加载器)
Webpack 默认只认识 JavaScript 和 JSON。 Loader 让 Webpack 能够处理其他类型的文件(CSS, 图片, TypeScript, Vue, JSX 等),并将它们转换为有效的模块
- css-loader :处理 CSS 中的
@import和url() - style-loader :将 CSS 注入到 DOM 的
<style>标签中 - babel-loader:将 ES6+ 代码转译为向后兼容的 JS
4.插件 (Plugin)
Loader 用于转换特定类型的模块 ,而 Plugin 则用于执行范围更广的任务。插件可以触及 Webpack 构建过程的每一个环节
- HtmlWebpackPlugin:自动生成 HTML 文件并自动引入打包后的 JS
- CleanWebpackPlugin :每次打包前自动清理
dist目录 - MiniCssExtractPlugin:将 CSS 提取为独立文件(而非打包在 JS 里)
5.模式 (Mode)
通过选择 development(开发)、production(生产)或 none,启用 Webpack 内置的优化
- Production 模式:自动开启代码压缩(UglifyJS)、Tree Shaking(摇树优化)
1.2. webpack 编译的全流程
Webpack 的打包过程:
1.2.1. 初始化 (Initialization)
- 读取配置 :Webpack 会读取你的
webpack.config.js,并与默认配置合并,得到最终的参数 - 创建 Compiler 对象 :用上一步的参数初始化
Compiler对象,它是 Webpack 的"总指挥部" - 加载插件 (Plugins) :遍历配置中的
plugins,依次调用它们的apply方法。此时,插件开始监听 Webpack 生命周期中的各个事件节点(钩子)
1.2.2. 第二阶段:编译构建 (Compilation) ------ 核心环节
这是 Webpack 最忙碌的阶段:
-
确定入口 (Entry):根据配置找到所有的入口文件。
-
编译模块 (Make):
-
从入口出发 ,调用匹配的 Loader 对文件进行转译(比如把
Sass转成CSS,把TS转成JS)。 -
解析依赖 :利用 AST(抽象语法树) 分析模块中的
import或require语句,找到它依赖的其他模块。 -
递归扫描:对依赖的模块重复上述过程,直到所有文件都被处理成 JS 模块。
-
-
完成模块编译 :得到每个模块被转译后的最终内容以及它们之间的依赖关系图 (Dependency Graph**)**。
1.2.3. 输出生成 (Seal & Emit)
- 组装 Chunk :根据入口和模块之间的依赖关系,将多个模块组合成一个个 Chunk(代码块)
- 转换成 Assets:把每个 Chunk 转换成一个单独的文件(Asset),并加入到输出列表
- 写入文件系统 (Emit) :确定好输出路径和文件名(
output.path&filename),将文件内容写入到硬盘的dist目录中
1.3. AST 是什么?
AST 是抽象语法树,将代码字符串解析成结构化的树形表示,每个节点代表代码中的一个语法结构
生成过程:
- 词法分析:将源代码拆分成 Token 流
- 语法分析:根据 Token 构建 AST
实际应用:
- Babel:源码 → AST → 转换 → 新代码
- ESLint:遍历 AST,检查违规模式
- Prettier:解析成 AST,再按规则重新打印
- Webpack:通过 AST 分析模块依赖
1.4. Webpack 的核心优势
- 模块化支持
Webpack 支持 ES Modules 、CommonJS 、AMD 等所有主流模块化标准,让你可以在前端项目中像在后端一样使用 require 或 import
- 强大的生态(Loader & Plugins)
几乎所有的前端需求(混淆、压缩、图片转 Base64、单元测试集成、代码校验)都有对应的插件或加载器
- 代码分割 (Code Splitting)
Webpack 可以将代码拆分成多个 Bundle
- 按需加载:只有用户访问某个路由时,才下载对应的 JS(懒加载)
- 公共代码提取 :将 React、Vue 等第三方库单独打成一个
vendor.js,充分利用浏览器缓存
- 热模块替换 (HMR)
在开发过程中,修改代码后,Webpack 只会替换发生变化的模块,而不需要刷新整个页面,极大地保留了应用当前的状态
1.5. plugin 和 loader 的区别
| 维度 | Loader (转换) | Plugin (插件) |
|---|---|---|
| 功能定位 | 专注文件转换。将 A 转换为 B | 专注流程控制。扩展 Webpack 功能 |
| 配置位置 | module.rules 下,通常关联 test 正则 | plugins 数组下,需要 new 实例 |
| 执行顺序 | 从右往左(或从下往上)链式调用 | 基于事件监听,由 Webpack 内部钩子触发 |
| 输出结果 | 返回转换后的字符串(JS 代码) | 直接修改 Compilation 对象或输出 Assets |
常用 Loaders:
babel-loader: ES6+ 转 ES5ts-loader: TypeScript 转 JSfile-loader / url-loader: 处理图片和字体
常用 Plugins:
HtmlWebpackPlugin: 自动生成 HTML 并引入打包后的 JSCleanWebpackPlugin: 每次构建前清理dist目录DefinePlugin: 编译时注入全局常量(如process.env.NODE_ENV)
1.6. Webpack 配置成使用 ES Module
-
让 Webpack 配置文件支持 ESM
-
修改后缀:将
webpack.config.js重命名为webpack.config.mjs。Node.js 会自动将.mjs文件视为 ESM -
修改
package.json:在你的package.json中添加"type": "module"。这样项目内所有的.js文件都会被默认视为 ESM -
使用转译工具:如果使用 TypeScript(
webpack.config.ts),可以配合ts-node自动处理模块转换
-
-
让 Webpack 输出 ESM
在 webpack.config.js 中开启 experiments.outputModule:
C++
export default {
// 1. 开启实验性特性
experiments: {
outputModule: true,
},
output: {
// 2. 指定库的导出格式为 'module'
library: {
type: 'module',
},
// 3. 必须指定环境,告诉 Webpack 支持 ES6 特性
environment: {
module: true,
},
filename: 'bundle.mjs',
},
};
- Webpack 对 ESM 的底层处理机制
Webpack 对 ESM 的支持不仅仅是格式问题,还涉及性能优化:
- Tree Shaking :ESM 是静态分析 的。Webpack 利用这一点,在构建阶段就能识别出哪些代码没被
import,从而在最终包中剔除它们 - Scope Hoisting (作用域提升):Webpack 会尽可能将所有模块合并到一个函数作用域中,减少闭包开销,提升运行速度
2. Vite
2.1. Vite 的核心工作原理
Vite 的性能优势源于它对开发环境 和生产环境采用了不同的处理策略。
- 开发环境:基于原生 ESM (No-Bundle)
利用浏览器原生支持的 ES Modules (ESM)。服务器启动时不需要打包
- 按需加载 :当浏览器解析到
import语句时,会向服务器发起请求。Vite 拦截请求,只对该文件进行实时编译并返回
-
依赖预构建 (Dependency Pre-bundling)
- esbuild :Vite 使用 Go 语言编写的 esbuild 预先将这些依赖打包成单个 ESM 模块
-
生产环境:基于 Rollup 打包
2.2. Vite常用配置
1.路径别名
JavaScript
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components')
}
}
2.代理配置
JavaScript
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
3.build.rollupOptions.manualChunks - 代码分割
作用:将第三方库拆分成独立 chunk,利用浏览器缓存
JavaScript
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['antd', '@mui/material'],
'utils': ['lodash', 'dayjs']
}
}
}
}
4.css.preprocessorOptions - 预处理器全局变量
作用:每个 SCSS 文件自动导入全局变量,无需手动 @import
JavaScript
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";` // 自动注入
}
}
}
5.css.modules - CSS Modules 配置
JavaScript
css: {
modules: {
localsConvention: 'camelCase', // 类名转驼峰
generateScopedName: '[name]__[local]__[hash:5]' // 自定义生成规则
}
}
6.optimizeDeps.include - 强制预构建
JavaScript
optimizeDeps: {
include: ['lodash-es', 'axios'], // 预构建这些依赖
exclude: ['your-local-lib'] // 排除某些依赖
}
2.3. Vite的代码分割
Vite 的代码分割基于 Rollup 的打包机制
核心原理:将原本打包成一个文件的代码,按照模块依赖关系、使用频率和更新频率,拆分成多个独立的 chunk 文件
底层机制:Rollup 的模块解析与分块算法
- 模块图构建(Module Graph)
Rollup 首先从入口文件开始,递归分析所有 import/export 语句,构建完整的模块依赖图
- 分块算法(Chunking Algorithm)
Rollup 根据以下规则决定如何分割:
- 入口点分割:每个
entry生成独立 chunk - 动态导入分割:
import()触发点自动生成新 chunk - 模块复用检测:被多个入口共享的模块提取为公共 chunk
- 手动控制:通过
manualChunks强制指定分组
代码分割是否越细越好,判断标准是什么
- 为什么"越细"反而可能"越慢"?
虽然细化分割可以减少单个包的体积,但会带来以下负面影响:
- HTTP 连结开销:虽然 HTTP/2 支持多路复用,但每个请求仍有 header 头部开销和浏览器解析开销。几百个 1KB 的 JS 小文件,加载效率远低于一个 100KB 的文件。
- 压缩率下降:Gzip 或 Brotli 压缩算法依赖于文本的重复性。文件分得太细,字典跨度变小,导致总体体积反而变大。
- 依赖瀑布流 :如果 A 分包依赖 B,B 又依赖 C,浏览器必须串行下载,导致首屏 TTI(可交互时间)显著延迟
- 维护成本上升:在几十个文件间跳转才能理解一个简单功能,违背了"高内聚"原则,增加了认知负担
- 判断标准:何时该分?
A. 变更频率(缓存命中率)
- 基础库 (Vendor):如 React、ReactDOM、AntD。这些库几乎半年不动,应该拆成独立的长效缓存包
- 业务逻辑:每天都在改,应该与基础库分离
B. 页面关联性(按需加载)
- 路由维度 :这是最基础的。用户访问
/home时,绝不应该加载/admin页面的 JS - 首屏无关性 :弹窗(Modal)、抽屉(Drawer)、复杂的图表库(如 Echarts)应该采用
dynamic import异步加载,只有用户点击时才下载
C. 共享程度 (Common Chunks)
- 多页复用:如果一个组件在 5 个页面中都被用到,且体积超过 30KB,就应该拆成独立的 Common Chunk
- 实战中的量化指标
- Bundle Analyzer :观察是否有巨大的"依赖孤岛"。如果某个包超过 200KB (Gzip),必须拆;如果小于 10K,建议合并
- Coverage 面板 :通过 Chrome DevTools 的
Coverage查看 JS 冗余率。如果首屏加载的 JS 有 60% 以上 未被执行,说明分割不够细 - 用户网络环境:在 4G/3G 弱网环境下测试。如果由于请求数过多导致明显的阻塞,说明分得太碎了
3. Vite vs Webpack
Webpack:打包式 HMR
- 所有模块被打包成 bundle:开发时也会打包,只是速度较慢
- 模块被 ID 标识:打包后每个模块有唯一 ID
- HMR runtime 注入 bundle:运行时替换模块代码
Vite:原生 ESM 式 HMR
- 不打包:直接输出原生的 ES Module
- 模块即文件:每个文件就是一个独立的模块
- 浏览器原生支持:通过
import动态加载
| 维度 | Webpack | Vite |
|---|---|---|
| 启动原理 | 全量构建:必须先抓取并编译所有模块,建立依赖图,最后才能启动服务器 | 按需构建:直接启动服务器,利用浏览器原生的 ES Modules (ESM) 能力,用到哪个文件才处理哪个文件 |
| 冷启动速度 | 较慢(数秒至数分钟) | 极快(毫秒级) |
| HMR 速度 | 随项目增大而变慢 | 极快(恒定速度) |
| 生产打包 | 自身 Bundle 打包 | 使用 Rollup 打包 |
| 依赖预编译 | 使用 JavaScript 编写的解析器处理所有依赖 | 使用 Go 语言 编写的 esbuild(快 10-100 倍) |
| 浏览器要求 | 支持旧版浏览器(需 Polyfill) | 开发环境要求支持原生 ESM 的现代浏览器 |
3.1. Vite 热更新(HMR)核心原理
Vite 的 HMR 基于浏览器原生 ES Module,核心链路是:文件监听 → 精确编译 → WebSocket 推送 → 动态导入 → 模块替换
整体架构
┌─────────────┐ WebSocket ┌─────────────┐
│ Vite Dev │ ◄─────────────────► │ 浏览器 │
│ Server │ (双向通信) │ (客户端) │
└─────────────┘ └─────────────┘
│ │
│ 1. 监听文件变化 │ 4. 动态导入新模块
▼ ▼
文件修改 替换旧模块
3.2. Vite 与 Webpack 的 HMR区别
| 环节 | Webpack HMR | Vite HMR |
|---|---|---|
| 触发更新 | 监听到文件变化,重新编译受影响的整个模块链路。 | 监听到文件变化,仅对该文件进行极速转译。 |
| 通信内容 | 发送包含 Hash 值和更新清单(manifest)的消息。 | 发送一个包含发生变动的文件路径的 JSON 指令。 |
| 浏览器响应 | 获取新的补丁(chunk.hot-update.js)并执行。 | 利用 import() 动态加载带时间戳的新模块:import('/src/App.tsx?t=xxx')。 |
| 性能损耗 | 由内而外:内部重新计算、打包、输出,压力在服务器。 | 由外而内:仅处理变动文件,剩下的交给浏览器按需加载。 |
4. ES6 向 ES5 的转换的方式
ES6 到 ES5 的转换主要通过 Babel 工具链完成。Babel 的核心工作流程分为三步:解析、转换、生成
Babel 是一个 JavaScript 编译器,它能将 ES6+ 代码转换为向后兼容的 ES5 版本,确保代码在旧版浏览器或环境中运行
- 解析 (Parse):使用
@babel/parser将 ES6 代码字符串解析成抽象语法树 (AST) - 转换 (Transform):使用
@babel/traverse遍历 AST,并通过插件对节点进行修改(如将let转为var、箭头函数转为普通函数),生成新的 ES5 结构的 AST - 生成 (Generate):使用
@babel/generator将转换后的 AST 重新生成为 ES5 代码字符串
现代化的替代方案:
- ESBuild (Go 编写):速度极快,常用于 Vite 的开发环境
- SWC (Rust 编写):Next.js 目前默认使用的编译器,性能远超 Babel
5. 配置优化打包速度或构建速度
5.1. webpack优化方案
- 缩小文件处理范围
resolve.extensions只配置必要后缀resolve.modules固定到 node_modulesresolve.alias减少递归查找module.noParse忽略已打包库(jQuery、lodash)
JavaScript
// webpack.config.js
module.exports = {
// 1. 精确控制 loader 处理范围
module: {
rules: [
{
test: /\.js$/,
// ✅ 只处理源代码,跳过 node_modules
include: path.resolve(__dirname, 'src'),
// ❌ 排除不需要处理的目录
exclude: /node_modules|dist|__tests__/,
use: ['babel-loader']
}
]
},
// 2. 减少 resolve 的搜索路径
resolve: {
// 只查找这些扩展名(越少越快)
extensions: ['.js', '.jsx', '.ts', '.tsx'],
// 固定查找路径,避免向上递归
modules: [path.resolve(__dirname, 'node_modules')],
// 设置别名,减少路径解析
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components')
},
// 避免解析这些库的依赖(它们已打包好)
noParse: /jquery|lodash|moment/
}
}
- 充分利用缓存机制
cache.type: 'filesystem'(Webpack5 内置缓存)babel-loader开启cacheDirectoryeslint-loader开启cache
JavaScript
// Webpack 5 内置缓存(最推荐)
module.exports = {
cache: {
type: 'filesystem', // 持久化缓存到磁盘
buildDependencies: {
config: [__filename] // 配置文件变化时失效缓存
},
cacheDirectory: path.resolve(__dirname, '.webpack_cache')
}
}
// Babel loader 缓存
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false // 不压缩缓存,读写更快
}
}]
}
// ESLint 缓存
{
test: /\.js$/,
use: [{
loader: 'eslint-loader',
options: {
cache: true,
cacheLocation: path.resolve(__dirname, '.eslint_cache')
}
}]
}
- 多进程/多线程并行处理
thread-loader耗时的 loader(babel、ts)(用 thread-loader 处理 babel-loade)terser-webpack-plugin开启parallel: true
JavaScript
// thread-loader - 将耗时 loader 放到线程池
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1, // CPU 核心数-1
workerParallelJobs: 50,
poolTimeout: 2000 // 闲置超时
}
},
'babel-loader'
]
}
]
}
}
// Terser 压缩并行(Webpack 5)
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true, // 开启多进程压缩
terserOptions: {
compress: { drop_console: true } // 生产环境删除 console
}
})
]
}
}
5.2. vite优化方案
-
依赖预构建
-
自动用 esbuild 预编译依赖
-
optimizeDeps.include手动指定
-
-
浏览器缓存
- 强缓存依赖,304 协商缓存源码
-
按需编译
- 只编译当前访问页面,其他页面不处理
C++
// vite.config.js
export default {
// 1. 优化依赖预构建
optimizeDeps: {
include: ['lodash-es', 'axios'], // 强制预构建
exclude: ['your-local-lib'], // 排除不需要预构建的
esbuildOptions: {
target: 'es2020' // 设置目标环境
}
},
// 2. 按需编译(默认开启)
server: {
fs: {
strict: false, // 允许访问根目录外文件(谨慎使用)
}
},
// 3. 构建优化
build: {
target: 'es2015',
minify: 'esbuild', // esbuild 比 terser 快 20 倍
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'], // 手动分包
utils: ['lodash-es', 'dayjs']
}
}
}
}
}