webpack:从 entry 走到浏览器实际加载的 chunks
核验日期:2026-07-07。示例版本来自
examples/build-tools/package.json:webpack5.108.4、webpack-cli7.2.1、html-webpack-plugin5.6.7。
需要掌握的问题
| 主题 | 读者必须回答的问题 | 原理与边界 | 高频错误 | 代码或观察证据 | 来源 |
|---|---|---|---|---|---|
entry → resolve → module rules | webpack 从哪里开始,怎样把 import 解析成模块? | entry 决定起点,resolve 决定请求到文件或包导出的映射,module.rules 决定资源进入哪些 Loader 或内置模块类型 | 默认 ./src/index.js 不存在、alias/exports 条件错、rules 匹配过宽 | 示例 entry: './src/main.ts'、resolve/rules 局部配置 | webpack concepts |
| Loader | Loader 为什么只处理单模块内容,pitch/normal 顺序怎样影响结果? | Loader 是接收单个 resource 内容并返回 JS/Buffer/map/meta 的函数;pitch 从左到右,normal 从右到左 | 在 Loader 里做全局分包、异步 callback 漏调用、缓存依赖漏登记 | pitch/normal 最小 Loader、this.addDependency | webpack loaders、Loader Interface |
| Plugin、Compiler、Compilation、Tapable | Plugin 如何介入完整生命周期? | Plugin 通过 apply(compiler) 注册 Tapable hooks;Compiler 管一次构建配置和运行,Compilation 管一次构建的模块、chunk、assets | 在 Loader 做全局工作、在异步 hook 里既 return Promise 又 callback、用已废弃 asset hook | compiler.hooks.thisCompilation、compilation.hooks.processAssets | webpack plugins、Compiler Hooks、Compilation Hooks |
module graph → chunk graph → runtime | 静态 import 和动态 import 如何变成主 JS 与 async chunk? | module graph 表示模块依赖,chunk graph 表示哪些模块进入哪些 chunk,runtime 负责加载异步 chunk 和 publicPath | 以为每个动态 import 都一定独立 chunk、HTML 直接加载 async chunk、runtime 被 CDN 缓存错 | stats.json 证明 detail.ts 在非 initial chunk | webpack concepts |
| splitChunks | 什么时候拆共享 chunk,拆错会怎样? | optimization.splitChunks 基于缓存组、大小、复用和入口关系抽取共享依赖 | 小项目过度拆包、旧 HTML 漏新 runtime/vendor、缓存命中但加载 404 | contenthash、runtimeChunk、cacheGroups 对照 | webpack caching guide |
| Tree Shaking / sideEffects | 未使用导出和副作用如何被判断? | 依赖 ESM、usedExports、minifier 和正确 sideEffects 声明;CSS/注册代码属于副作用 | sideEffects: false 删除 CSS 或全局注册、只在 development 测试 | SHOULD_NOT_REACH_BUNDLE 不应出现在产物中 | webpack tree shaking |
| Source Map 与 stats | 如何证明产物可追溯,而不是只看命令退出 0? | devtool 控制 Source Map;stats 可输出 assets、chunks、modules、reasons 等结构化构建证据 | 生产暴露源码、map 链断裂、stats 太粗看不到 async chunk | devtool: 'source-map'、stats.chunkModules | devtool、stats |
| filesystem cache、watch、HMR | 冷构建、增量、热更新分别依赖什么状态? | filesystem cache 持久化编译结果;watch 根据依赖失效;HMR 通过 dev server 和 runtime 接受模块更新 | 缓存键漏环境变量、Loader 未登记外部依赖、HMR accept/dispose 不完整 | cache 配置、this.addDependency、HMR gotcha | cache、HMR guide |
| publicPath、hash、CDN | 浏览器加载失败时怎样定位是路径、hash 还是缓存问题? | emitted assets 通过 output.publicPath 被引用;contenthash 绑定内容;HTML 是入口清单 | CDN 仍缓存旧 HTML 指向已删除 hash、publicPath 指向错误 origin | Network 404、HTML script/link、stats assets | publicPath |
| 迁移与重大变化 | webpack 4 到 5、5.108 哪些变化会改变工程决策? | webpack 5 引入持久化缓存、资产模块、改进 tree shaking、移除自动 Node polyfill;5.108 继续推进 experimental HTML/CSS/TypeScript 能力 | 把实验能力当默认稳定能力、升级后 Node 内置模块缺失 | 迁移表、实验标注、配置 diff | webpack 5 release、webpack 5.108 |
内容边界说明:
| 分类 | 本文落点 |
|---|---|
| 编程语言与类型系统 | 解释 JS/TS 模块如何进入 graph;示例使用 webpack 5 experimental TypeScript,只做构建解析,不等于完整类型检查 |
| Browser APIs 与持久化能力 | 覆盖浏览器 script/link、动态 import、Source Map、HTTP/CDN 缓存、HMR WebSocket;不讨论 IndexedDB 或权限 API |
| UI 框架与元框架 | 只说明 webpack 作为底层打包器供框架工具使用;组件调度、渲染和水合不在本文重复 |
| 构建工具与运行时 | 完整覆盖输入、解析、转换、依赖图、分块、优化、缓存、插件、产物和故障定位 |
| 后端、数据与 Agent 技术 | 只覆盖 Loader/Plugin/config 以开发者权限执行和 CI 信任边界;服务端请求生命周期、数据库一致性和 Agent 评测不适用 |
entry、resolve、module rules:先把请求变成模块
webpack 的入口问题是:从哪个模块开始构建 dependency graph。默认入口是 ./src/index.js,真实项目通常显式写入口,避免目录结构变化时猜错。
共享示例的入口:
// examples/build-tools/configs/webpack.config.mjs
export default {
context,
mode: 'production',
target: ['web', 'es2022'],
entry: './src/main.ts',
};main.ts 里有三类依赖:
import { label, tools } from './catalog.ts';
import './side-effect.ts';
import './styles.css';
button.addEventListener('click', async () => {
const { renderDetail } = await import('./detail.ts');
renderDetail(detail, tool);
});这会形成三种边:
| 边 | 进入 graph 的方式 | 产物影响 |
|---|---|---|
| 静态 JS/TS import | 编译入口同步依赖 | 进入 initial chunk |
| CSS 副作用 import | 被 experiments.css 或 Loader 处理 | 生成 CSS asset 或运行时样式 |
| 动态 import | 创建异步依赖块 | 进入非 initial async chunk |
resolve 负责把请求映射成文件。最小配置通常长这样:
export default {
resolve: {
extensions: ['.ts', '.js'],
conditionNames: ['browser', 'import', 'default'],
alias: {
'@': new URL('../src/', import.meta.url).pathname,
},
},
};module.rules 负责选择 Loader 或内置资源类型:
export default {
module: {
rules: [
{ test: /\.svg$/i, type: 'asset/resource' },
{ test: /\.css$/i, use: ['style-loader', 'css-loader'] },
],
},
};本仓库示例没有写传统 ts-loader 和 CSS Loader,而是启用 webpack 5 的实验能力:
experiments: {
css: true,
typescript: true,
}这不是“webpack 默认稳定支持所有 HTML/TypeScript/CSS 应用构建”的声明。webpack 5.108 官方把 HTML、CSS、TypeScript 方向明确放在 opt-in experiment 叙事里;生产项目迁移时要单独跑回归。
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
Module not found: Can't resolve './src' | 未配置 entry,默认入口不存在 | pnpm --filter @roadmap/build-tools-slice build:webpack 看首个 resolve 错误 | 显式写 entry,不要依赖默认目录 |
| 同一个包打进两份 | resolve 条件、alias 或 symlink 策略不一致 | stats 中搜索重复 package 路径 | 统一 resolve.conditionNames、alias、workspace 依赖入口 |
| CSS 不出现 | 没有 Loader,也没启用 experiments.css | 搜索 stats assets 是否有 .css | 使用稳定 Loader/插件,或明确启用实验 CSS 并验收 |
Loader:pitch 从左到右,normal 从右到左
Loader 的职责很窄:处理一个模块的 resource 内容。它不是全局优化器,也不应该决定 chunk 结构。
一个同步 Loader:
export default function bannerLoader(source, inputMap) {
this.cacheable(true);
this.addDependency(this.resourcePath);
return `/* built by bannerLoader */\n${source}`;
}一个异步 Loader:
export default function asyncLoader(source) {
const callback = this.async();
someAsyncRead(this.resourcePath, (error, extra) => {
if (error) return callback(error);
callback(null, `${extra}\n${source}`);
});
}一个 pitch Loader 可以短路后续 Loader:
export function pitch(remainingRequest) {
if (this.resourceQuery.includes('raw')) {
return `export default ${JSON.stringify(`-!${remainingRequest}`)};`;
}
}Loader 难点完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| 构建卡住不结束 | 调了 this.async() 但没有调用 callback | 给 Loader 加超时日志或最小 fixture | 每条分支都 callback;不要同时 return Promise 和 callback |
| watch 改外部文件不重编译 | Loader 读取了额外文件但没 this.addDependency | 改被读取文件,看 watch 是否触发 | 对文件用 addDependency,对目录用 addContextDependency |
| filesystem cache 命中旧结果 | Loader 结果依赖 process.env 或网络,但没进入缓存键 | 改 env 后比较输出 hash | 把环境写入 options/version,或 cacheable(false) |
| Loader 里想改多个 chunk | Loader 只处理当前模块内容 | 搜索 Loader 是否访问 compilation.chunks | 改成 Plugin,在 Compilation hook 里做全局工作 |
Plugin、Compiler、Compilation、Tapable 生命周期
Plugin 解决的问题是:当你需要跨模块、跨 chunk 或改 assets 时,单模块 Loader 不够用。
class AssetListPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('AssetListPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'AssetListPlugin',
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
},
(assets) => {
const body = Object.keys(assets).sort().join('\n');
compilation.emitAsset(
'asset-list.txt',
new compiler.webpack.sources.RawSource(body),
);
},
);
});
}
}生命周期分工:
| 对象 | 生命周期 | 能看见什么 | 典型用途 |
|---|---|---|---|
Compiler | 一份 webpack 配置的一次运行或 watch 容器 | options、resolver、文件系统、hooks、watch 状态 | 初始化插件、注册全局 hooks、创建 Compilation |
Compilation | 一次具体构建 | modules、chunks、assets、errors、warnings、module graph、chunk graph | 分析模块、改 assets、生成报告 |
Tapable hooks | 同步、异步、瀑布、bail 等 hook 协议 | hook 参数决定上下文 | 控制插件执行时机和异步完成语义 |
Plugin | 用户传入 plugins 数组的对象 | apply(compiler) 入口 | 介入 webpack 完整生命周期 |
异步 hook 严重问题:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| 产物偶发缺文件 | tapAsync 中提前 callback,异步写入还没完成 | 在 processAssets 打印开始/结束时间 | 使用 tapPromise 并 await,或最后再 callback |
| webpack 5 报废弃 hook | 仍使用 optimizeAssets、additionalAssets 等旧资产 hook | 开启 warnings,读 compilation warnings | 改用 compilation.hooks.processAssets 和 stage |
| persistent cache 报序列化问题 | Plugin 把不可序列化对象塞进模块/asset metadata | 看 cache warning 和 plugin 最近改动 | 只存可序列化数据,或用官方 compiler.webpack.sources 类型 |
module graph、chunk graph 与 runtime:动态导入不是 HTML 标签
webpack 先建立 module graph:哪些模块依赖哪些模块。随后建立 chunk graph:哪些模块放进哪些 chunk。最后 runtime 负责在浏览器里加载异步 chunk。
关键边界:
| 机制 | 解决的问题 | 不保证 |
|---|---|---|
| module graph | 找出所有可达模块和依赖关系 | 每个模块都一定进入同一个文件 |
| chunk graph | 把模块分配到 initial、async、shared chunks | 每个 dynamic import 都独占一个文件 |
| runtime | 在浏览器执行模块包装、加载 async chunk、维护模块缓存 | CDN 一定有文件、publicPath 一定正确 |
| HTML 插件或 HTML experiment | 把 initial assets 注入 HTML | 不应该直接 script 加载 async chunk |
共享示例要求 detail.ts 是非 initial chunk。观察命令:
pnpm --filter @roadmap/build-tools-slice build:webpack
node -e "const s=require('./examples/build-tools/dist/webpack/stats.json'); console.log(s.chunks.map(c => ({id:c.id, initial:c.initial, files:c.files, modules:(c.modules||[]).map(m=>m.name||m.nameIdentifier)})))"预期结果:至少一个 initial: false 的 chunk,其 modules 或嵌套 modules 能追溯到 src/detail.ts;index.html 只加载 initial JS/CSS,不直接加载该 async chunk。
splitChunks、Tree Shaking 与 sideEffects:优化会改变语义边界
splitChunks 解决“共享依赖不要重复下载”的问题。Tree Shaking 解决“可证明没用的导出不要进入最终产物”的问题。sideEffects 解决“哪些模块即使导出没用也必须执行”的问题。
export default {
optimization: {
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
},
},
},
},
};取舍表:
| 方案 | 性能 | 可靠性 | 使用难度 | 维护成本 | 适用规模 |
|---|---|---|---|---|---|
| 不配 splitChunks | 小项目请求少、配置少 | 简单可靠 | 低 | 低 | 示例、很小应用 |
| 默认 splitChunks | 避免明显重复 | webpack 默认经验值较稳 | 中 | 中 | 普通应用 |
| 自定义 vendor/runtime | 长期缓存更稳定 | 配错会增加请求和旧 HTML 风险 | 中高 | 中高 | 多页面、大型应用 |
| 激进 sideEffects false | 体积可能更小 | 容易误删 CSS/注册代码 | 高 | 高 | 仅适合可证明纯 ESM 包 |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| CSS 或全局注册消失 | 包级 sideEffects: false 误删副作用模块 | 搜索产物是否有 CSS asset 和 preserved 文本 | 用数组保留 ["*.css", "./src/register.js"] |
SHOULD_NOT_REACH_BUNDLE 仍在产物中 | ESM 静态分析失败或 minify 未执行 | rg "SHOULD_NOT_REACH_BUNDLE" dist/webpack | 保留 ESM,开启 production/minimize,检查 CommonJS 包 |
| vendor hash 每次都变 | runtime 或 module id 不稳定 | 连续两次构建比较 asset 名 | 使用 deterministic ids、抽 runtime、避免构建时间写入 bundle |
| 初始请求过多 | splitChunks 过度拆分 | DevTools Network 看 initial JS 数量和 waterfall | 放宽 cacheGroups,按真实复用和 HTTP/2/3 条件调整 |
Source Map 与 stats:用证据证明产物结构
devtool 控制 Source Map。示例使用:
export default {
devtool: 'source-map',
stats: {
all: false,
assets: true,
chunks: true,
chunkModules: true,
modules: true,
},
};Source Map 取舍:
| devtool 思路 | 适用 | 风险 |
|---|---|---|
source-map | 生产需要外部 .map 调试 | 公开 .map 会暴露源码 |
hidden-source-map | 上传错误平台,不在 JS 里暴露 URL | 发布系统要保证 map 与 hash 对齐 |
eval-* | 开发快速 rebuild | 不适合生产,安全策略和可读性差 |
false | 极简或高度保密发布 | 事故定位困难 |
stats 不是装饰品,它是构建证据。共享测试会用 stats.json 验证:
const stats = JSON.parse(await readFile('examples/build-tools/dist/webpack/stats.json', 'utf8'));
const asyncDetail = stats.chunks.find((chunk) =>
chunk.initial === false &&
chunk.modules?.some((module) =>
/(?:^|[\\/])detail\.ts/.test(module.nameIdentifier ?? module.name ?? ''),
),
);错误完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| 浏览器错误只指向压缩 JS | 没生成外部 Source Map 或没部署 | 检查 JS 尾部 sourceMappingURL 和 .map 文件 | 设置 devtool: 'source-map' 或错误平台上传 hidden map |
| stats 看不到 chunk modules | stats 配置太少 | jq '.chunks[0]' stats.json | 打开 chunks 和 chunkModules |
| map sources 泄露绝对路径 | loader 或 devtool 输出了本机路径 | `rg "$PWD | file:" dist/webpack` |
filesystem cache、watch 与 HMR:三种“快”的边界不同
webpack 的快有三种:
| 能力 | 作用 | 状态来源 | 常见误判 |
|---|---|---|---|
| filesystem cache | 跨进程复用编译结果 | 源码、配置、依赖、版本、cache buildDependencies | 命中不代表结果一定符合新环境 |
| watch | 文件变化后局部重建 | 文件/目录依赖图 | Loader 漏 addDependency 会不触发 |
| HMR | 浏览器运行时替换模块 | dev server、HMR runtime、模块 accept/dispose | HMR 成功不代表生产 bundle 正确 |
最小 filesystem cache:
export default {
cache: {
type: 'filesystem',
version: process.env.BUILD_TARGET ?? 'local',
buildDependencies: {
config: [new URL('./webpack.config.mjs', import.meta.url).pathname],
},
},
};HMR 最小边界:
if (import.meta.webpackHot) {
import.meta.webpackHot.accept('./detail.ts', async () => {
const next = await import('./detail.ts');
console.log('detail module updated', next);
});
}严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
切换 BUILD_TARGET 后仍用旧产物 | filesystem cache version 或 buildDependencies 漏环境 | 清缓存前后比较 bundle 内容 | 把环境写进 cache.version 或禁用跨环境 cache |
| watch 不重建 Sass/模板 include | Loader 没登记二级文件依赖 | 改 include 文件,观察 rebuild | Loader 调 this.addDependency 或 this.addContextDependency |
| HMR 后点击仍是旧函数 | DOM 事件 handler 持有旧闭包 | 浏览器控制台看 HMR 日志和交互行为 | 在 accept 回调中重新绑定,必要时 dispose 清理 |
| 开发正常生产失败 | dev server 内存产物、publicPath、minify、splitChunks 与生产不同 | 同时跑 production build 和静态预览 | 不用 HMR 结果替代生产验收 |
publicPath、contenthash 与 CDN:加载链路的严重问题
webpack 输出的文件不等于浏览器一定能加载。浏览器先拿 HTML,再按 HTML 和 runtime 拼出的 URL 请求 JS/CSS/async chunk。
export default {
output: {
filename: 'assets/[name].[contenthash:8].js',
chunkFilename: 'assets/[name].[contenthash:8].js',
cssFilename: 'assets/[name].[contenthash:8].css',
publicPath: './',
clean: true,
},
};加载链路故障表:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| 首屏 JS 404 | output.publicPath 指到错误 CDN/origin/path | DevTools Network 看请求 URL;检查 stats.publicPath | 按部署路径设置 publicPath,或使用 auto 并验证 |
| 点击后 async chunk 404 | runtime 拼出的 chunk base path 错 | 只点击触发动态 import,观察 chunk URL | 修正 publicPath,不要只看初始 HTML |
| 部署后用户拿旧 HTML 指向已删除 hash | CDN/浏览器缓存 HTML 太久,clean 删除旧 asset | 查看旧 HTML 中 hash 是否还存在于 dist/CDN | HTML 短缓存,assets 长缓存;保留上个版本 assets 或原子发布 |
| CSS 200 但样式不生效 | HTML 未引用 CSS 或 MIME/cache 错 | 查看 index.html link 和 Response headers | 确保 CSS asset 被注入,CDN 返回正确 Content-Type |
| Source Map 404 或错配 | map 没随 hash 上传,或清理策略删了 map | 检查 JS 尾部 map 文件名 | 上传同版本 map;生产可改 hidden map |
webpack 4 到 5,再到 5.108:迁移和重大变化
webpack 5 的重大变化集中在长期缓存、Node polyfill、资产模块、Module Federation、内部 graph/runtime 和持久化缓存。webpack 5.108 继续推进 universal target、experimental HTML modules、CSS、Tree Shaking 和 typed config。
| 变化 | webpack 4 常见状态 | webpack 5 / 5.108 状态 | 迁移动作 |
|---|---|---|---|
| Node polyfill | 许多 Node 内置模块自动 polyfill | webpack 5 移除自动 Node polyfill | 浏览器代码删除 Node 依赖,或显式 resolve.fallback |
| 持久化缓存 | 主要依赖 loader cache 或外部缓存 | 内置 filesystem cache | 配置 cache.buildDependencies,防止环境漏 key |
| 资产处理 | file-loader、url-loader 常见 | Asset Modules 内置 | 小步替换 Loader,比较 URL、hash、大小 |
| Graph 内部 | plugin 可能读旧模板对象 | module graph / chunk graph / runtime modules 更明确 | 迁移 Plugin 到新 API,少碰内部私有属性 |
| HTML | 主要依赖 html-webpack-plugin | 5.107/5.108 有 experimental HTML modules 和 HTML output | 不把实验能力当默认;生产继续验收插件方案 |
| TypeScript | 常用 ts-loader、babel-loader、fork-ts-checker | 5.107+ 有 experimental TypeScript support | 明确“不等于完整类型检查”;保留 tsc --noEmit |
| CSS | css-loader、style-loader、mini-css-extract-plugin 常见 | 原生 CSS 能力仍需实验标注和限制核查 | 项目逐项迁移,不一次性删 Loader |
升级严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
crypto、stream、buffer 找不到 | webpack 5 不再自动 polyfill Node 内置模块 | stats errors 看 resolve 请求 | 删除浏览器端 Node 依赖;必要时显式 fallback |
自研 Plugin 访问 compilation.assets 报警告 | webpack 5 资产处理迁移到 processAssets | 构建 warnings | 改 compilation.emitAsset/updateAsset 和 processAssets stage |
| 启用 HTML experiment 后页面行为和插件方案不同 | experimental HTML modules 与 html-webpack-plugin 尚非完全等价 | 对比 HTML、chunks、HMR、模板能力 | 只在实验分支试用;生产按回归结果决定 |
| TypeScript 构建过了但类型错 | webpack 构建解析 TS 不等于类型检查 | 单独跑 tsc --noEmit | 把类型检查留给 TypeScript 工具链 |
共享示例:webpack 构建浏览器入口
运行命令:
pnpm --filter @roadmap/build-tools-slice build:webpack输入:
examples/build-tools/src/main.ts
examples/build-tools/src/catalog.ts
examples/build-tools/src/detail.ts
examples/build-tools/src/side-effect.ts
examples/build-tools/src/styles.css预期结果:
examples/build-tools/dist/webpack/index.html
examples/build-tools/dist/webpack/stats.json
examples/build-tools/dist/webpack/assets/*.js
examples/build-tools/dist/webpack/assets/*.css
examples/build-tools/dist/webpack/assets/*.js.map
examples/build-tools/dist/webpack/assets/*.css.map验证观察:
find examples/build-tools/dist/webpack -type f | sort
node --test examples/build-tools/test/artifacts.test.mjs预期输出要点:
| 观察点 | 预期结果 |
|---|---|
index.html | 通过相对 script src 加载 initial JS,通过 link rel="stylesheet" 加载 CSS |
stats.json | 存在非 initial chunk,并且该 chunk 能追溯到 detail.ts |
| 主 JS source map | sources 至少能追溯到 main.ts |
| async JS source map | sources 至少能追溯到 detail.ts |
| CSS source map | sources 至少能追溯到 styles.css |
| bundled JS | 包含 Babel 和 preserved,不包含 SHOULD_NOT_REACH_BUNDLE |
如果只运行 webpack 命令退出 0,还不能证明产物正确;至少要检查 HTML 引用、async chunk、CSS、Source Map、stats 和运行时可见文本。