跳到主要内容

webpack 分析优化

1. Tree Shaking

Tree Shaking 是一种通过移除未使用代码(dead code)来减小打包体积的技术。确保在生产环境的配置中启用 Tree Shaking,Webpack 会自动进行,但前提是使用 ES6 模块语法(import/export)。

tree-shaking 原理

  1. 静态导入分析:Tree Shaking 的前提是 ES2015+ 的模块语法。与 CommonJS 的动态导入不同,ES2015+ 模块的导入和导出在代码编写时就已确定,不会在运行时改变。这使得构建工具可以在编译阶段静态分析代码,准确知道哪些模块、函数或变量被导入和使用,并构建依赖图
  2. 标记未使用的导出:在构建过程中,Webpack 等工具会遍历所有模块的依赖树,分析每个模块的导出是否在其他地方被引用。对于未被引用的导出,构建工具会将其标记为“未使用”。
  3. 删除未使用的导出:在最终生成的打包文件中,构建工具会删除那些被标记为“未使用”的代码。对于 Webpack 来说,这一步通常是在压缩阶段由 Terser(或其他压缩工具)完成的,因为 Terser 能够识别和移除未被引用的代码。

源码实现

math.js

export const add = (a,b) => a + b;
export const subtract = (a,b) => a - b;

index.js

import { add } from 'math'

const res = add(1,2)
console.log(res) // output: 3

伪代码

// treeShakingSimulator.js
const fs = require('fs');

// 假设的依赖图,表示 index.js 依赖了 math.js 中的 add 函数
const dependencyGraph = {
'index.js': ['add'],
'math.js': ['add', 'subtract']
};

// 模拟 Tree Shaking 过程
function shakeTree(entry, graph) {
const usedExports = graph[entry];
const moduleContent = fs.readFileSync('./math.js', 'utf-8');

// 简化:直接基于依赖图移除未使用的导出
let shakenContent = moduleContent;
for (const exportName of Object.keys(graph['math.js'])) {
if (!usedExports.includes(exportName)) {
// 移除未使用的导出(这里使用简单的字符串替换模拟)
const regex = new RegExp(`export const ${exportName} = \\(.*?\\) => .*?;\\n`, 'g');
shakenContent = shakenContent.replace(regex, '');
}
}

fs.writeFileSync('./shakenMath.js', shakenContent);
console.log('Tree shaking completed. Result written to shakenMath.js');
}

// 执行模拟的 Tree Shaking
shakeTree('index.js', dependencyGraph);

请注意,这个简化的示例仅用于演示 Tree Shaking 的概念,实际的 Tree Shaking 过程要复杂得多,涉及到 AST(抽象语法树)分析、死代码检测和更精细的代码处理。在实际项目中,这一过程由构建工具(如 Webpack 或 Rollup)自动完成。

2. 代码分割和懒加载

利用 Webpack 的代码分割(Code Splitting)特性,将应用拆分成多个 chunks,然后按需加载。对于 Vue 应用,可以针对路由进行代码分割,利用 Vue Router 的动态导入特性:

const Foo = () => import('./Foo.vue')

代码分割

代码分割是指将应用的代码分成多个块(chunks),然后根据需要加载这些块。现代前端构建工具,如 Webpack、Rollup 和 Parcel,提供了内置支持来实现代码分割。

原理

  1. 构建时分析:构建工具在打包应用时,会分析模块之间的依赖关系,识别出可以分割成独立块的代码部分。
  2. 生成多个块:根据分析结果,构建工具会将应用代码分割成多个块(通常是 JavaScript 文件)。这些块包括入口块(包含应用的启动代码)和多个次级块(包含特定功能或页面的代码)。
  3. 按需加载:应用运行时,只加载入口块。当用户交互或应用逻辑需要额外的代码时,相应的次级块会被动态加载。

懒加载

懒加载是一种特定类型的代码分割,它更侧重于应用运行时的行为。懒加载确保某些代码块或资源只有在真正需要时才被加载和执行。

实现懒加载:

在 JavaScript 或框架(如 Vue、React)中,懒加载通常通过动态导入(Dynamic Imports)实现。

  • 动态导入:使用特殊的语法(如 import() 函数)动态地导入模块。这告诉构建工具该模块应该被分割出来,并在需要时才加载。
// 示例:动态导入一个模块
button.addEventListener('click', async () => {
const module = await import('./someModule.js');
module.doSomething();
});

在 Vue 或 React 应用中,路由配置通常是实现懒加载的理想场所,因为不同的路由对应不同的页面组件,这些组件可以按需加载。

webpack 中 import() 函数

Webpack 的 import() 函数实现了 ECMAScript 提案中的动态导入功能,允许在运行时按需加载模块。这一特性是实现代码分割和懒加载的关键。import() 函数返回一个 Promise 对象,该对象在模块加载完成时解析。

运行机制

  1. 代码分析:在构建过程中,Webpack 分析源代码,寻找 import() 语句。每个 import() 调用都被视为一个分割点(split point),Webpack 会为每个分割点创建一个新的代码块(chunk)。
  2. 创建额外的代码块:对于每个 import() 调用,Webpack 生成一个或多个额外的代码块。这些代码块包含了被动态导入的模块及其所有依赖。
  3. 加载和解析:运行时,当执行到 import() 语句时,Webpack 会异步加载相应的代码块。加载完成后,解析 Promise,并执行模块的导出代码。
  4. 公共模块提取:如果多个动态导入的模块依赖了相同的模块,Webpack 会尝试将这些公共依赖提取到单独的 chunk 中,以避免重复加载相同的代码。
  5. 网络请求:动态加载的代码块通过网络请求获取。Webpack 确保按需加载的模块及其依赖项以正确的顺序被加载和执行。

webpack 应用

  • 配置分割点:Webpack 的 SplitChunksPlugin 允许开发者定义代码分割的规则,例如,可以指定将来自 node_modules 的库分割到单独的块中。
  • 动态导入:Webpack 支持 import() 语法来实现模块的动态导入,这是实现懒加载的关键。

3. 分析和优化包大小

使用 webpack-bundle-analyzer 分析打包后的文件大小,识别和移除不必要的依赖。

分析报告

运行 Webpack 构建时,webpack-bundle-analyzer 会自动生成一个 Web 服务,并在浏览器中打开一个包含你的打包分析结果的页面。这个页面展示了每个 bundle 和 chunk 的大小,以及它们之间的关系。

  1. 识别大模块
  • 使用分析报告识别出体积较大的模块或库。对于这些大模块,考虑以下优化策略:
  • 确认是否真正需要这些库或模块的全部功能,或许只用到了它们的一小部分。如果是后者,考虑仅导入需要的部分。
  • 检查是否有更轻量级的替代方案。
  • 对于第三方库,考虑使用 CDN 引入,而不是打包到应用中。
  1. 优化代码分割
  • 利用 Webpack 的代码分割功能,确保应用按需加载代码。特别是对于单页应用(SPA),使用路由级的动态导入来分割代码:
const SomeComponent = () => import('./SomeComponent.vue');
  1. 移除未使用的代码
  • 按需引入:确保项目中没有未使用的代码和库。对于一些大型库,如果你只使用了它们的一小部分,考虑使用更细粒度的导入。
  • tree-shaking: 使用 Tree Shaking 特性移除未使用的代码。

4. 配置 externals

Webpack 的 externals 配置项允许你定义一些不希望被打包进最终 bundle 的外部依赖,即使这些依赖在项目源码中通过 import 或 require 被引用。

当 Webpack 处理到这些外部依赖时,它不会将这些依赖的代码打包进输出文件,而是在运行时(runtime)通过指定的全局变量来访问这些依赖。

module.exports = {
// 其他配置...
externals: {
jquery: 'jQuery'
}
};

在这个配置中,jquery 指的是你的源代码中通过 import 或 require 引用的模块名称,而 'jQuery' 是这个库在全局作用域中的变量名。在 HTML 文件中,你需要通过 <script> 标签引入 jQuery:

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

externals 实现原理

  1. 标记外部依赖:在 Webpack 配置中,通过 externals 字段指定哪些模块不应该被 Webpack 处理。你可以将其设置为一个对象、函数、正则表达式等,以适应不同的排除需求。
  2. 跳过模块解析和打包:在构建过程中,Webpack 会跳过这些被标记为外部的模块,不对它们进行解析或打包。
  3. 在输出的代码中保留引用:尽管外部模块不会被打包,Webpack 仍然会在生成的代码中保留对这些模块的引用。这意味着在运行时,这些模块需要以其他方式提供,通常是通过 <script> 标签直接引入到 HTML 页面中,或者通过 Node.js 的全局模块系统提供。
  4. 运行时替换:在应用运行时,这些外部依赖会被替换为 Webpack 配置中指定的全局变量。例如,你可以指定 jQuery 作为一个外部依赖,并通过 CDN 在 HTML 页面中引入。Webpack 生成的代码会在运行时期望在全局作用域中找到 jQuery,并使用它而不是打包进来的副本。