--- id: frontend.webpack.build-optimization title: webpack 5:loader 与 plugin、splitChunks、tree shaking、缓存、模块联邦 type: lesson domain: frontend stack: [webpack] tags: [webpack, loader, plugin, splitChunks, tree shaking, contenthash, 持久化缓存, Module Federation] level: intermediate prerequisites: [] est_hours: 2.5 related: [frontend.vite.dev-build-config, frontend.webpack.scenario.build-speed, frontend.webpack.interview] week: [7] order: 1 --- # webpack 5:loader 与 plugin、splitChunks、tree shaking、缓存、模块联邦 > 基于 webpack 5.112 + webpack-cli 7 编写并实际构建验证。很多公司的存量项目仍在用 webpack,面试也常问原理。以官方文档为准:https://webpack.js.org/concepts/ 。 ## 解释说明 ### 核心概念 | 概念 | 一句话 | | --- | --- | | entry | 从哪里开始分析依赖 | | output | 产物放在哪里、叫什么名字 | | **loader** | 把非 JS 文件(或需要转换的 JS)**转换**成 JS 模块;从右到左(从下到上)链式执行 | | **plugin** | 在编译**生命周期的钩子**上做事(生成 HTML、压缩、提取 CSS、分析体积),基于 tapable 事件机制 | | chunk | 打包过程中的代码块;入口、动态 `import()`、`splitChunks` 都会产生 chunk | | mode | `development` / `production`,决定默认优化项 | ### 构建流程(面试常问) 初始化参数 → 创建 Compiler → 从 entry 开始,对每个模块调用匹配的 loader 转换 → 解析出依赖,递归构建模块依赖图 → 根据入口和拆包规则组装 chunk → 生成代码写入 output。过程中在各个阶段触发钩子,plugin 在钩子上执行。 ### 优化手段 - **tree shaking**:依赖 ES 模块的静态结构,`usedExports` 标记未使用的导出,压缩时删除;`package.json` 的 `sideEffects` 声明哪些文件有副作用(如 CSS),其余可以整体删除。 - **splitChunks**:提取公共代码和第三方库;`runtimeChunk: "single"` 把运行时单独拆出。 - **contenthash**:文件名随内容变化,配合 CDN 长期缓存。 - **持久化缓存**:`cache: { type: "filesystem" }`,第二次构建明显变快。 - **构建提速**:缩小 loader 的处理范围(`include` / `exclude`)、用 swc / esbuild 类 loader 代替 babel、多进程、减少不必要的 source map。 - **Module Federation(模块联邦)**:多个独立构建的应用在运行时共享模块,用于微前端。 ## 案例 一个主站和后台两个入口共享工具函数:通过 `splitChunks` 提取出 `shared`,运行时拆出 `runtime`,报表模块用动态 `import()` 按需加载。 ## 代码示例 完整项目:[examples/webpack-config](examples/webpack-config/),`npm install && npm run build` 输出: ```text [size] runtime.981552ae.js 2.74 KiB [size] main.38cd14b7.js 0.33 KiB [size] 531.30ecc347.js 0.21 KiB ← 动态 import 的 report.js [size] admin.e2a620fd.js 0.20 KiB [size] shared.5f4a3ab4.js 0.18 KiB ← 两个入口共用的 utils.js Entrypoint main 3.25 KiB = runtime + shared + main Entrypoint admin 3.12 KiB = runtime + shared + admin ``` 已验证:`utils.js` 中未被使用的 `unusedHelper` 在产物中搜索不到(tree shaking 生效)。 自定义 loader([loaders/md-loader.cjs](examples/webpack-config/loaders/md-loader.cjs)): ```js module.exports = function mdLoader(source) { const { trim = false } = this.getOptions(); const text = trim ? source.trim() : source; return `export default ${JSON.stringify(text)};`; }; ``` 自定义 plugin([webpack.config.js](examples/webpack-config/webpack.config.js)): ```js class SizeReportPlugin { apply(compiler) { compiler.hooks.done.tap("SizeReportPlugin", (stats) => { const assets = stats.toJson({ all: false, assets: true }).assets ?? []; for (const a of assets) console.log(`[size] ${a.name} ${(a.size / 1024).toFixed(2)} KiB`); }); } } ``` ## 易错点 - 混淆 loader 和 plugin 的职责。 - loader 的执行顺序写反(`use: ["style-loader", "css-loader"]` 中 css-loader 先执行)。 - 用了 CommonJS(`require` / `module.exports`)导致 tree shaking 失效;`sideEffects: false` 配置错误导致 CSS 被删掉。 - 用 `[hash]` 而不是 `[contenthash]`,每次构建所有文件名都变,缓存失效。 - 生产环境开启了暴露源码的 source map 并公开访问。 ## 面试常问 - loader 和 plugin 的区别?写过吗? - webpack 的构建流程? - tree shaking 的原理和失效原因? - 怎么优化构建速度和产物体积?