Appearance
Webpack 原理与优化
构建流程 · Loader/Plugin · 文件指纹 · HMR · 构建提速 · 分片优化 · 路由懒加载
一、Webpack 构建流程
- 初始化参数(合并配置)
- 创建 Compiler 对象,注册插件
- 从 entry 入口解析文件构建 AST,找出依赖
- 递归调用 Loader 编译模块
- 组装 Chunk,生成输出资源
- 写入文件系统
Compiler vs Compilation:
- Compiler:webpack 启动时一次性创建,包含完整配置环境(options/loaders/plugins),整个构建生命周期只存在一个
- Compilation:一次资源版本的构建,开发模式下文件变化会创建新的 Compilation
二、Loader 与 Plugin 区别
- Loader:文件转换器,运行在打包前,将 A 文件编译为 B 文件(如 scss → css)
- Plugin:扩展 webpack 功能,在整个编译周期起作用,监听事件广播执行任务
- 常用 Loader:style-loader、css-loader、less-loader、babel-loader、file-loader、url-loader
自定义 Loader 实现
Loader 本质:一个导出函数的 JS 模块,接收源文件内容(source),返回编译后的内容
js
// replace-loader.js
const { getOptions } = require('loader-utils')
module.exports = function(source) {
const options = getOptions(this) // 获取 webpack 配置中的 options
return source.replace(/NAME/g, options.words)
}Loader 调用顺序(pitch 机制):
- 配置
use: ['style-loader', 'css-loader', 'sass-loader'] - 先从左往右执行各 loader 的
pitch方法,再从右往左执行 loader 主函数 - 若某个
pitch返回值,则中断后续调用,反向执行前面的 loader
style-loader(pitch) → css-loader(pitch) → sass-loader(pitch)
↓
style-loader ← css-loader ← sass-loader(主函数)Loader 常用 API:
this.async():异步 loader,返回 callbackthis.cacheable(false):禁用缓存(默认缓存)loader.raw = true:处理二进制数据(Buffer)getOptions(this):获取配置传入的 options
mini style-loader 原理(pitch 用法):
- 在
pitch阶段返回一段 JS 代码字符串 - 这段代码创建
<style>标签,通过require(!!remainingRequest)获取 css-loader 处理后的内容 !!前缀表示跳过所有 loader 直接 require- 将 CSS 内容插入
<style>标签并 append 到<head>
自定义 Plugin 实现
Plugin 组成:
- 一个 JavaScript 命名函数
- 在原型上定义
apply方法 - 注册 webpack 的事件钩子(Tapable)
- 处理 webpack 内部实例的特定数据
- 功能完成后调用 webpack 提供的回调
自定义 Plugin 示例(生成构建文件列表):
js
class FileListPlugin {
constructor(options) {
this.filename = options.filename || 'filelist.md'
}
apply(compiler) {
// 注册 emit 钩子(输出文件前触发)
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
const files = Object.keys(compilation.assets)
let content = `${files.length} files in this build:\n\n`
files.forEach(f => { content += `* ${f}\n` })
// 将新资源插入构建结果
compilation.assets[this.filename] = {
source: () => content,
size: () => content.length
}
cb()
})
}
}
module.exports = FileListPlugin三、文件指纹策略
- hash:以项目为单位,项目内容变则 hash 变
- chunkhash:以 chunk 为单位,对应 chunk 内文件变则 hash 变
- contenthash:以自身内容为单位
- 推荐:CSS 用 contenthash,JS 用 chunkhash
四、Webpack 热更新(HMR)原理
- webpack-dev-server 创建两个服务:静态资源服务(express)+ Socket 服务(websocket)
- 文件变化时,Socket 服务生成 manifest.json + update chunk.js
- 通过 websocket 推送给浏览器
- 浏览器通过 HMR runtime 加载更新模块
五、提升 Webpack 构建速度
- 缩小 loader 处理范围:用
include/exclude限定范围,避免处理node_modulesjs{ test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ } - resolve.alias:直接指定模块路径,跳过递归搜索
- module.noParse:跳过无依赖库的解析(如 jQuery 不依赖其他包)js
module: { noParse: /jQuery/ } - IgnorePlugin:忽略第三方包的指定目录(如 moment.js 的 locale 文件)js
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) // moment 包体积从 263KB 降至 55KB - 使用 DLLPlugin 预编译不常变的库
- 使用 cache-loader / 构建缓存
- terser 多线程压缩
- 生产环境关闭 SourceMap:
productionSourceMap: false
六、分片优化(splitChunks 实战)
分片原则:
node_modules中的文件打包进chunk-vendors,变动频率低,可利用 304 缓存- 被引用少且体量大的库单独分一个 chunk(如 echarts、tinymce)
- 公共代码分成多个 chunk,避免从某入口访问时下载全部公共代码
- 体量很小的异步 chunk 合并进相关 chunk 或入口 chunk
路由懒加载决策矩阵:
| 使用频率高 | 使用频率低 | |
|---|---|---|
| 文件小 | 合并进入口 chunk | 单独一个 chunk |
| 文件大 | 单独一个 chunk(加 prefetch) | 单独一个 chunk |
cacheGroups 配置示例:
js
splitChunks: {
maxInitialRequests: 5,
maxAsyncRequests: 6,
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
minChunks: 2,
chunks: 'all',
},
echarts: {
name: 'chunk-echarts',
test: /[\\/]node_modules[\\/]echarts[\\/]/,
priority: 0,
chunks: 'all',
},
common: {
name: true,
minChunks: 2,
minSize: 60000,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true,
},
},
}chunk-vendors vs chunk-common 策略不同:vendors 是第三方库,变动少可用 304 长期缓存;common 是项目代码,经常变动缓存易失效,因此 common 应拆分而非合并
七、路由懒加载
js
{
path: '/user/XXX',
component: () => import(/* webpackChunkName: "chunk-user-XXX" */ 'src/pages/XXX'),
}路由懒加载原理(Webpack JSONP 机制)
import() 编译后,Webpack 生成以下核心机制:
window["webpackJsonp"]:全局数组,各 chunk 通过.push()注册自身模块webpackJsonpCallback:拦截 push 方法,将 chunk 中的模块缓存到modules对象,并更新installedChunks[chunkId] = 0(标记已加载)__webpack_require__(moduleId):执行模块,带缓存(installedModules)避免重复执行__webpack_require__.e(chunkId)(核心):通过 JSONP 动态加载 chunk- 创建
<script>标签,src指向对应 chunk 文件 - 三种加载状态:
0(已完成)、undefined(未加载/失败/超时)、Promise(加载中) - 超时处理:120s 超时后 reject Promise,抛出
ChunkLoadError
- 创建
完整流程:
用户访问路由 → import() 触发 → webpackAsyncContext 查 map 找到 chunkId
→ __webpack_require__.e(chunkId) 创建 <script> 加载 JS
→ chunk 加载完成执行 webpackJsonpCallback → 模块注册到 modules
→ __webpack_require__(moduleId) 执行模块 → 渲染页面八、预加载策略(Preload / Prefetch)
- preload:告诉浏览器当前页面马上要用到的文件,高优先级下载
- prefetch:告诉浏览器空闲时提前下载用户接下来可能用到的文件
- 策略:preload
chunk-vendors+ 入口 chunk;prefetch 常用 asyncChunks - 注意:preload 要放在 prefetch 之前;prefetch 不应滥用(占用带宽)
- 如果
<script>有crossorigin属性,prefetch/preload 也必须有,否则资源会重复下载
相关
- 组件库与 npm 包 — 按需引入、组件库搭建、npm 发布
- 微前端与 Monorepo — 架构设计、pnpm workspace
- 性能优化专题 — 构建层优化