ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

面试题:如何优化 Webpack 的打包速度?

面试题:如何优化 Webpack 的打包速度? 一、核心思路一句话Webpack 打包慢的本质是构建阶段需要处理的模块太多、处理过程太重、重复工作太多优化就是减少处理量、提高并行度、利用缓存并缩小需要解析和构建的范围。二、主要矛盾和次要矛盾主要矛盾Webpack 构建过程大量消耗时间的核心通常是模块数量多 ↓ 依赖分析 ↓ Loader 转换 ↓ 代码解析 / 编译 ↓ 代码压缩 ↓ 重复构建所以真正需要优化的是减少需要处理的模块减少每个模块的处理成本避免重复处理让可以并行的任务并行执行次要矛盾例如手动删除几个无用变量单纯减少源码体积修改一些不影响构建过程的配置这些可能有帮助但通常不是大型项目构建变慢的主要原因。三、解决方案流程图Webpack 打包慢 │ ▼ 先定位到底慢在哪里 │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ 模块太多 Loader慢 压缩慢 │ │ │ ▼ ▼ ▼ 缩小处理范围 优化 Loader 并行压缩 │ │ │ ├─ include │ ├─ exclude │ └─ 减少重复处理 │ ├───────────────┐ ▼ ▼ 缓存 并行处理 │ │ ▼ ▼ filesystem缓存 thread-loader │ 或其他并行方案 │ └───────────────┐ ▼ 外部依赖优化 │ ┌──────────┴──────────┐ ▼ ▼ externals CDN │ │ ▼ ▼ 不参与打包 减少构建内容四、底层实现原理Webpack 到底在忙什么理解这个问题首先要知道 Webpack 的核心工作流程。入口文件 ↓ 解析模块 ↓ 发现 import / require ↓ 继续递归解析依赖 ↓ 形成依赖图 ↓ Loader 转换模块 ↓ Plugin 参与构建 ↓ 生成 Chunk ↓ 代码优化 ↓ 压缩 ↓ 输出文件因此Webpack 处理的模块越多、Loader 越复杂、Plugin 越多、压缩越重构建时间通常就越长。所以优化不是简单地“让 Webpack 更快”而是尽量让 Webpack 少干活并且同样的活不要重复干。五、具体优化方案1. 使用缓存避免重复构建这是非常重要的一项。Webpack 5 可以使用持久化文件缓存constpathrequire(path);module.exports{mode:development,cache:{// 将构建缓存保存到文件系统。// 下一次构建时如果相关依赖没有发生变化// Webpack 可以复用之前的构建结果。type:filesystem,// 缓存版本发生变化时可以主动失效。buildDependencies:{config:[__filename],},},entry:./src/index.js,output:{path:path.resolve(__dirname,dist),filename:bundle.js,},};原理第一次源代码 ↓ 解析 ↓ Loader ↓ 构建 ↓ 生成缓存第二次源代码 ↓ 检查缓存 ↓ 没有变化 ↓ 直接复用缓存因此第一次构建较慢 第二次构建 ↓ 命中缓存 ↓ 少做大量重复工作 ↓ 更快使用场景尤其适合本地开发增量构建大型项目模块数量非常多的项目六、Loader 要限制处理范围这是面试中非常值得讲的一点。例如 Babelmodule.exports{module:{rules:[{test:/\.js$/,include:path.resolve(__dirname,src),exclude:/node_modules/,use:babel-loader,},],},};这里最重要的是exclude:/node_modules/为什么假设项目源码1000 个模块 node_modules10000 个模块如果 Babel 对大量第三方代码重复进行处理10000 个第三方模块 ↓ Babel解析 ↓ AST转换 ↓ 代码生成构建时间自然会上升。如果明确只处理 src ↓ 不处理 node_modules就可以大幅减少工作量。更准确的面试表达不要简单说“排除 node_modules 就一定能加快。”应该说对于确定不需要经过某个 Loader 转换的依赖可以通过 include/exclude 缩小 Loader 的处理范围减少无意义的解析和转换。因为某些第三方依赖可能确实需要经过 Babel 或其他 Loader 处理不能机械地全部排除。七、多线程让 CPU 密集型任务并行Webpack 5 本身提供了更完善的缓存和构建能力但并不是所有 Loader 都自动变成多线程执行。例如可以使用thread-loader将部分 Loader 放到 Worker 池中执行。constpathrequire(path);module.exports{module:{rules:[{test:/\.js$/,include:path.resolve(__dirname,src),use:[{loader:thread-loader,options:{// 创建 Worker 线程池。workers:2,},},{loader:babel-loader,},],},],},};原理普通情况主线程 │ ├── Babel 模块1 ├── Babel 模块2 ├── Babel 模块3 └── Babel 模块4并行主线程 │ ┌───────┼───────┐ ↓ ↓ ↓ Worker1 Worker2 Worker3 │ │ │ 模块1 模块2 模块3如果任务本身具有较高 CPU 计算量并且并行收益能够覆盖 Worker 通信和调度成本就可能明显降低构建时间。边界不是所有项目都适合多线程。如果项目很小任务本身很快 ↓ 创建 Worker ↓ 线程通信 ↓ 调度成本反而可能没有收益。所以正确说法是针对 CPU 密集型且任务量足够大的 Loader 或构建任务可以考虑并行处理小项目不应该为了多线程而多线程。八、代码压缩阶段可以并行生产环境中代码压缩可能成为明显的耗时点。例如使用支持并行压缩的方案让多个资源能够并行处理。核心思想JS文件 │ ┌─────┼─────┐ ↓ ↓ ↓ Worker Worker Worker ↓ ↓ ↓ 压缩1 压缩2 压缩3这比笼统地说“Webpack 打包使用多线程”更加准确。因为真正应该定位到底是哪一步 CPU 占用高然后针对具体阶段进行并行化。九、减少无用代码有用但不是主要优化手段ES Module 静态依赖 ↓ Webpack 分析模块 ↓ Tree Shaking ↓ 识别未使用的导出 ↓ 在优化阶段删除无用代码对“打包速度”的影响如果项目存在大量真正不需要参与构建的源码减少这些代码确实可以减少解析 ↓ 依赖分析 ↓ Loader处理 ↓ 优化但对于一个规范项目而言手动删除几个无用变量通常不是解决几分钟甚至几十分钟构建时间的核心手段。十、externals让某些依赖不参与打包例如module.exports{externals:{react:React,react-dom:ReactDOM,},};意味着业务代码 │ ├── import React │ ▼ Webpack │ └── 不把 React 打进 Bundle运行时由外部环境提供浏览器 │ ├── CDN加载 React │ └── CDN加载 ReactDOM原理正常业务代码 ↓ React ↓ Webpack ↓ Bundleexternals业务代码 ↓ Webpack ↓ Bundle React ─────────→ 外部提供因此可以减少 Bundle 体积减少 Webpack 需要处理的依赖在特定架构下减少构建工作量边界externals并不是“用了就一定更好”。它意味着运行环境必须可靠地提供这个依赖。否则最终运行时可能Webpack构建成功 ↓ 浏览器运行 ↓ React不存在 ↓ 运行时报错所以它更适合企业内部平台多应用共享基础库CDN 统一提供依赖特定微前端架构十一、第三方依赖不要重复解析可以通过include exclude noParse externals等方式缩小处理范围。例如module.exports{module:{noParse:/jquery|lodash/,},};noParse 的含义如果确定某个第三方库不需要 Webpack 解析其内部依赖可以告诉 Webpack不要继续解析这个文件。这样可以减少依赖解析工作。但要特别注意noParse不是“这个包不参与打包”。而是这个模块仍然可能被打包只是不再解析它内部的依赖关系。这和externals完全不同。对比noParse ↓ 仍然可能进入 Bundle ↓ 但不继续分析内部依赖externals ↓ 不进入 Bundle ↓ 由外部环境提供这是面试很容易被追问的点。十二、分析工具不要盲目优化真正高级的回答应该先说先分析再优化。可以使用speed-measure-webpack-pluginWebpack 的构建统计信息webpack-bundle-analyzerChrome Performance操作系统 CPU / 内存监控重点回答Webpack为什么慢 ↓ 到底慢在哪一步 ↓ Loader Plugin 模块解析 代码压缩 模块数量 重复构建 ↓ 针对瓶颈优化这比上来就说“我使用多线程。”更体现工程化思维。十三、完整优化架构Webpack 构建性能优化 │ ┌─────────────┼─────────────┐ ↓ ↓ ↓ 减少工作量 提高并行度 避免重复工作 │ │ │ ┌──────┼──────┐ │ ┌─────┴─────┐ ↓ ↓ ↓ ↓ ↓ ↓ include exclude noParse 多线程 filesystem 增量构建 缓存 │ │ └──────┬──────┘ ↓ 第三方依赖优化 │ ┌──────┴──────┐ ↓ ↓ externals CDN │ ↓ 减少依赖处理 │ ▼ 最后优化压缩阶段 │ ▼ 构建完成十四、使用场景场景一本地开发越来越慢优先考虑Webpack 5 filesystem cache 缩小 Loader 处理范围 减少不必要 Plugin场景二生产构建需要十几分钟先分析构建时间 ↓ 模块解析 Loader 代码压缩 Plugin如果Babel占大量时间考虑include/exclude 缓存 并行处理如果代码压缩占大量时间则重点优化压缩器 并行压缩 减少需要压缩的代码场景三大型 Monorepo重点考虑缓存 增量构建 任务并行 减少跨包重复构建 合理拆分构建边界这种场景下“删除几个无用变量”基本不是核心优化方向。十五、面试中的常见误区误区一项目体积越小Webpack 打包一定越快。不够准确。应该说参与构建的模块数量、解析范围和处理复杂度会直接影响构建时间最终产物体积只是其中一个相关因素。误区二ESLint 会自动删除所有无用代码。不准确。ESLint 的主要职责是代码检查而 Tree Shaking 是Webpack ES Module 静态依赖分析 优化阶段误区三Webpack 5 自动多线程。不准确。应该说Webpack 5 提供了更完善的缓存等能力对于 CPU 密集型任务可以通过 Loader、压缩工具或其他构建工具提供的并行能力进行优化。误区四externals和noParse是一回事。不是。externals 不参与 Bundle由外部提供 noParse 仍然可以参与 Bundle但不解析内部依赖十六、如果让我在项目中实际优化我会怎么做第一步测 ↓ 找到真正的耗时瓶颈 ↓ 第二步减少 ↓ include / exclude / noParse ↓ 减少不必要模块和 Loader 工作 ↓ 第三步缓存 ↓ Webpack filesystem cache ↓ 避免重复构建 ↓ 第四步并行 ↓ CPU密集型任务并行 ↓ 第五步外置 ↓ externals / CDN ↓ 减少第三方依赖参与构建 ↓ 第六步重新测量 ↓ 确认优化是否真正有效这套流程比“背几个 Webpack 配置项”更重要。十七、满分答案Webpack 打包优化的核心不是简单减少代码而是减少构建阶段需要做的工作同时避免重复工作。Webpack打包慢 ↓ 先定位瓶颈 ↓ ┌──────────┬──────────┬──────────┐ ↓ ↓ ↓ 模块太多 Loader慢 压缩慢 ↓ ↓ ↓ 缩小范围 include 并行 ↓ exclude 压缩 noParse 缓存 ↓ 第三方依赖 ↓ externals / CDN ↓ Webpack filesystem cache ↓ 减少重复构建具体来说我一般从四个方向优化第一减少工作量通过include、exclude、noParse限制 Loader 和模块解析范围避免处理不需要处理的代码。第二提高并行度对于 Babel、代码压缩这类 CPU 密集型任务可以合理使用并行处理但要考虑线程通信成本小项目不应该盲目多线程。第三利用缓存Webpack 5 可以开启 filesystem cache让没有变化的模块复用之前的构建结果特别适合大型项目的增量构建。第四减少第三方依赖参与构建在合适的场景下使用externals配合 CDN把稳定的公共依赖交给外部环境提供。另外我不会一上来就改配置而是先分析构建耗时到底集中在哪个阶段再针对瓶颈优化。因为真正决定优化效果的不是配置项数量而是减少了多少实际构建工作。一句话收尾Webpack 性能优化的本质就是——少解析、少转换、少压缩、少重复并把能并行的任务并行起来。
返回列表