# 如何更好地优化打包资源

> 随着前端的发展，特别是 React，Vue 等构造单页应用的兴起，前端的能力得以很大提升，随之而来的是项目的复杂度越来越大。此时的前端的静态资源也越来越庞大，而毫无疑问 javascript 资源已是前端的主体资源，对于压缩它的体积至为重要。

- 发布于: 2020-02-25
- 更新于: 2026-09-30
- 原文: https://shanyue.tech/posts/frontend-engineering/bundle/
- 标签: frontend-engineering
- 专栏: 前端工程化

---

在前端中但凡谈到打包，肯定要提及到 `webpack`，毕竟它现在已经是最为流行的打包工具。但 `webpack` 更多地是表现在 `术` 上，于是我决定写这篇文章，更多地讲解一些关于 `道` 的。

对于一个前端而言，生产环境的静态资源优化，它既是面试中的高频问题，同时也最容易成为平时工作中的 OKR/KPI。如果你经常致力于优化前端打包提及，必然会对一些数字极为敏感，比如：

1. `lodash` 和 `react` gzip 后的体积是多少 (定性，可以给出范围)
1. 打包 `moment` 时会有什么问题
1. 你们线上前端项目首屏静态资源 gzip 后的体积是多少

如果你负责了你们前端项目的打包优化，如果以上问题连一个都不了解那么是说不通的。以我作为面试官的两年经验中，如果候选人对这些问题有所了解的话，往往对打包以及webpack的了解就会相对深入一些

## 原则

一般谈到打包会有两方面的意思，第一在于提高打包的速度，第二在于对打包后的静态资源的优化。**而对于静态资源的优化又不仅仅是打包提及的缩减。**

对于打包资源优化的总体原则，在于尽可能的减少或者延迟模块的引用。主要遵循以下三点

1. 减小打包的整体体积
1. `Code Splitting`: 按需加载，优化页面首次加载体积。如根据路由按需加载，根据是否可见按需加载
1. `Bundle Splitting`：分包，根据模块更改频率分层次打包，充分利用缓存

接下来本篇文章将会结合实例分别阐述这三点

## 01 减小打包的整体体积

![](https://cloud.githubusercontent.com/assets/302213/20628702/93f72404-b338-11e6-92d4-9a365550a701.gif)

第一种方法是减小打包的整体体积。减小打包的总体积有多种方式，这往往也是打包资源优化的着力点，一方面操作性高易于实践，~另一方面有具体数据支撑易于写PPT来晋升~。我从网站性能优化的实践角度，来分为以下几个方面

### 代码压缩

代码压缩可以非常可观地减小资源打包体积，但是它的可操作性空间过小。可操作性低的意思是这一项不太容易出现在晋级评审的PPT上，如同 CDN 在网站性能优化的重要程度一样，重要但不归你做(或者傻瓜式配置)。

它良好的模块化，以致于 `webpack` 就自作主张在生产环境中默认把这件事给做了。

那它是如何压缩代码的？最典型的两种方法就是空白符替换以及缩短变量名，如代码所示，仅仅通过这两种方式就大大压缩了 javascript 资源：

```js
// 压缩前
function sum(first, second) {
  return first + second;
}

// 压缩后
function s(x, y) {
  return a + b;
}
```

关于代码压缩，可以参考山月的[前端高级进阶系列](https://github.com/shfshanyue/blog/tree/master/frontend-engineering)之[javascript 代码的体积是如何被压缩的](https://github.com/shfshanyue/blog/blob/master/frontend-engineering/uglify.md)

### 移除不必要的模块

这句话好像是废话，但它却是真正有用并且极为容易实现的一点。

在以下代码中，对 `lodash` 这个模块进行了引入，但在之后的代码中并无使用 `lodash`，那在 webpack 中这个模块还会继续打包吗？

很遗憾，仍会对它进行打包。但好消息是这一点优化起来相当简单。

```js
// 仅仅引入而未在代码中使用，该模块仍然会被打包
import _ from "lodash";
```

对于这类问题总应该防患于未然，扼杀于摇篮中。`eslint` 的用武之地来了，它除了统一团队的代码风格以外，也用来提高团队的代码质量以及性能。

### 选择可替代的体积较小的模块

针对这一条，有一个典型的例子是以体积过大而臭名昭著的 `moment.js` 模块，它仅仅用于 `DateTime` 的格式化及各种计算。但你 `import` 之后它的体积竟然达到了 200kb+，gzip 后仍然有 69kb。以至于在 github 上有一个仓库专门用来介绍如何优化它，

- [How to optimize moment.js with webpack](https://github.com/jmblog/how-to-optimize-momentjs-with-webpack)

再来一张图感受一下它巨大的体积吧：

![](https://raw.githubusercontent.com/jmblog/how-to-optimize-momentjs-with-webpack/master/source-map-explorer.png)

此时可以选择一个可替代它功能，但体积更小的模块。与 `moment.js` API 兼容的 `day.js`，它 gzip 后体积仅仅只有 2kb。

### 按需引入模块

当你面对一个巨无霸的，捆绑式的大型模块时，可能你并不会使用到它的所有的功能，你只需要按照你的需求引入模块就可以了。那经常会有哪些巨无霸模块呢？

如 `lodash` (勉强算)，`antd`，`echarts`，我相信这三个模块对于以 React 为主的前端工程师都或多或少使用过。对你所需要使用的模块单独引入：

```js
import DatePicker from "antd/es/date-picker"; // for js
import "antd/es/date-picker/style/css"; // for css

import get from "lodash.get";
```

## 02 Code Splitting: 按需加载，优化页面首次加载体积

懒加载，如果面试中提到懒加载的话，大概率面试官此时是想问你关于图片懒加载的问题。

> [前端开发中的图片懒加载如何实现](https://q.shanyue.tech/fe/html/1.html)

通过 `Code Splitting` 可以只加载当前所需要的核心资源：

1. 如果你处在首页，并且首页中有占用资源过重的图表，需要对图表懒加载，否则它会大幅拖垮应用的首次渲染，加大白屏时间
1. 如果你处在首页，你无需加载当前不可见屏幕下方的复杂组件
1. 如果你处在页面 A，你没有必要加载页面 B 的资源

他们实现起来均需要额外编写代码，所以可操作性中等，但是好在它能够带来极大的益处，投资回报率较高，操作起来也极为简单，接下来就属于体力活了：

- 使用 `import()` 动态加载模块
- 使用 `React.lazy()` 动态加载组件
- 使用 `lodable-component` 动态加载路由，组件或者模块

大部分情况下，你只要做一个莫得感情的 API 工程师调用以上三个 API 就可以解决问题，大幅度降低页面的首次加载体积。但是在前往高级前端工程师的路上，你有可能需要了解其中的原理，~~(有可能并不需要，数据比原理重要)~~ 来做更加精细化的控制，比如针对缓存。

> [Code Splitting 的原理是什么？](https://q.shanyue.tech/fe/webpack/206.html)

## 03 Bundle Splitting

除了资源体积上的优化，另一个大的优化就是缓存。单页应用有一个最好的方面，就是所有资源都是带有指纹信息的，这意味着所有的资源都是能够设置永久缓存的。

但仅仅如此了吗？

如果你所有的 js 资源都打包成一个文件，它确实有永久缓存的优势。但是当有一行文件进行修改时，这一个大包的指纹信息发生改变，永久缓存失效。

所以我们现在需要做到的是：当修改文件后，造成最小范围的缓存失效，这样便能够更充分的利用缓存，减小宽带，减小服务器费用。一个好消息是 `webpack` 等打包工具虽然在 `optimization` 上内置了很多性能优化，但它不会帮你做这件事，它并不知道你有哪些模块，以及这些模块的重要紧急程度，你终于可以大展拳脚了。

此时我们可以对资源进行分层次缓存的打包方案，这是一个建议方案

1. `webpack-runtime`: 应用中的 `webpack` 的版本比较稳定，分离出来，保证长久的永久缓存
1. `react-runtime`: `react` 的版本更新频次也较低
1. `vendor`: 常用的第三方模块打包在一起，如 `lodash`，`classnames` 基本上每个页面都会引用到，但是它们的更新频率会更高一些

随着 `http2` 的发展，特别是多路复用，初始页面的静态资源不受资源数量的影响。因此为了更好的缓存效果以及按需加载，也有很多方案建议把所有的第三方模块进行单模块打包。

在 webpack 中，使用 `splitChunks.cacheGroups`

```js
{
  splitChunks: {
    cacheGroups: {
      react: {
        test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
        name: 'react',
        chunks: 'all'
      },
      vendor: {

      }
    }
  },
  runtimeChunk: {
    name: entrypoint => `runtime-${entrypoint.name}`,
  },
}
```

## 小结

毫无疑问在前端中更好地优化打包资源属于网站性能优化强操作性部分的重中之重，整理下本篇文章关于资源优化的所有内容

1. 减小打包的整体体积
   - 代码压缩
   - 移除不必要的模块
   - 按需引入模块
   - 选择可以替代的体积较小的模块
1. `Code Splitting`: 按需加载，优化页面首次加载体积。如根据路由按需加载，根据是否可见按需加载
   1. 使用 `import()` 动态加载模块
   1. 使用 `React.lazy()` 动态加载组件
   1. 使用 `lodable-component` 动态加载路由，组件或者模块
1. `Bundle Splitting`：分包，根据模块更改频率分层次打包，充分利用缓存
