Skip to main content

打包

常见问题

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

原则

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

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

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

减小打包的整体体积

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

代码压缩

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

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

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

移除不必要的模块

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

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

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

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

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

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

How to optimize moment.js with webpack

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

现在 monentjs 可以换成 dayjs 或者 date-fn 了

按需引入模块

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

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

Code Splitting: 按需加载,优化页面首次加载体积

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

前端开发中的图片懒加载如何实现

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

如果你处在首页,并且首页中有占用资源过重的图表,需要对图表懒加载,否则它会大幅拖垮应用的首次渲染,加大白屏时间 如果你处在首页,你无需加载当前不可见屏幕下方的复杂组件 如果你处在页面 A,你没有必要加载页面 B 的资源 他们实现起来均需要额外编写代码,所以可操作性中等,但是好在它能够带来极大的益处,投资回报率较高,操作起来也极为简单,接下来就属于体力活了:

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

Code Splitting 的原理是什么?

Bundle Splitting

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

但仅仅如此了吗?

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

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

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

webpack-runtime: 应用中的 webpack 的版本比较稳定,分离出来,保证长久的永久缓存 react-runtime: react 的版本更新频次也较低 vendor: 常用的第三方模块打包在一起,如 lodash,classnames 基本上每个页面都会引用到,但是它们的更新频率会更高一些 随着 http2 的发展,特别是多路复用,初始页面的静态资源不受资源数量的影响。因此为了更好的缓存效果以及按需加载,也有很多方案建议把所有的第三方模块进行单模块打包。

在 webpack 中,使用 splitChunks.cacheGroups

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

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