这是山月关于高级前端进阶暨前端工程系列文章的第 M 篇文章 (M 随便打的,毕竟也不知道能写多少篇),关于前 M-1 篇文章,可以从我的 github repo shfshanyue/blog[1] 中找到,如果点进去的话可以捎带~点个赞~,如果没有点进去的话,那就给这篇文章点个赞。。今天的文章开始了
本篇文章地址在 前端工程化系列[2],欢迎订阅。
在前端中但凡谈到打包,肯定要提及到 webpack
,毕竟它现在已经是最为流行的打包工具。但 webpack
更多地是表现在 术
上,于是我决定写这篇文章,更多地讲解一些关于 道
的。
对于一个前端而言,生产环境的静态资源优化,它既是面试中的高频问题,同时也最容易成为平时工作中的 OKR/KPI。如果你经常致力于优化前端打包提及,必然会对一些数字极为敏感,比如:
lodash
和 react
gzip 后的体积是多少 (定性,可以给出范围)moment
时会有什么问题如果你负责了你们前端项目的打包优化,如果以上问题连一个都不了解那么是说不通的。以我作为面试官的两年经验中,如果候选人对这些问题有所了解的话,往往对打包以及webpack的了解就会相对深入一些
一般谈到打包会有两方面的意思,第一在于提高打包的速度,第二在于对打包后的静态资源的优化。而对于静态资源的优化又不仅仅是打包提及的缩减。
对于打包资源优化的总体原则,在于尽可能的减少或者延迟模块的引用。主要遵循以下三点
Code Splitting
: 按需加载,优化页面首次加载体积。如根据路由按需加载,根据是否可见按需加载Bundle Splitting
:分包,根据模块更改频率分层次打包,充分利用缓存接下来本篇文章将会结合实例分别阐述这三点
第一种方法是减小打包的整体体积。减小打包的总体积有多种方式,这往往也是打包资源优化的着力点,一方面操作性高易于实践,~另一方面有具体数据支撑易于写PPT来晋升~。我从网站性能优化的实践角度,来分为以下几个方面
代码压缩可以非常可观地减小资源打包体积,但是它的可操作性空间过小。可操作性低的意思是这一项不太容易出现在晋级评审的PPT上,如同 CDN 在网站性能优化的重要程度一样,重要但不归你做(或者傻瓜式配置)。
它良好的模块化,以致于 webpack
就自作主张在生产环境中默认把这件事给做了。
那它是如何压缩代码的?最典型的两种方法就是空白符替换以及缩短变量名,如代码所示,仅仅通过这两种方式就大大压缩了 javascript 资源:
// 压缩前
function sum (first, second) {
return first + second;
}
// 压缩后
function s(x,y){return a+b}
关于代码压缩,可以参考山月的前端高级进阶系列[3]之javascript 代码的体积是如何被压缩的[4]
这句话好像是废话,但它却是真正有用并且极为容易实现的一点。
在以下代码中,对 lodash
这个模块进行了引入,但在之后的代码中并无使用 lodash
,那在 webpack 中这个模块还会继续打包吗?
很遗憾,仍会对它进行打包。但好消息是这一点优化起来相当简单。
// 仅仅引入而未在代码中使用,该模块仍然会被打包
import _ from 'lodash'
对于这类问题总应该防患于未然,扼杀于摇篮中。eslint
的用武之地来了,它除了统一团队的代码风格以外,也用来提高团队的代码质量以及性能。
针对这一条,有一个典型的例子是以体积过大而臭名昭著的 moment.js
模块,它仅仅用于 DateTime
的格式化及各种计算。但你 import
之后它的体积竟然达到了 200kb+,gzip 后仍然有 69kb。以至于在 github 上有一个仓库专门用来介绍如何优化它,
再来一张图感受一下它巨大的体积吧:
此时可以选择一个可替代它功能,但体积更小的模块。与 moment.js
API 兼容的 day.js
,它 gzip 后体积仅仅只有 2kb。
当你面对一个巨无霸的,捆绑式的大型模块时,可能你并不会使用到它的所有的功能,你只需要按照你的需求引入模块就可以了。那经常会有哪些巨无霸模块呢?
如 lodash
(勉强算),antd
,echarts
,我相信这三个模块对于以 React 为主的前端工程师都或多或少使用过。对你所需要使用的模块单独引入:
import DatePicker from 'antd/es/date-picker'; // for js
import 'antd/es/date-picker/style/css'; // for css
import get from 'lodash.get'
懒加载,如果面试中提到懒加载的话,大概率面试官此时是想问你关于图片懒加载的问题。
前端开发中的图片懒加载如何实现[6]
通过 Code Splitting
可以只加载当前所需要的核心资源:
他们实现起来均需要额外编写代码,所以可操作性中等,但是好在它能够带来极大的益处,投资回报率较高,操作起来也极为简单,接下来就属于体力活了:
import()
动态加载模块React.lazy()
动态加载组件lodable-component
动态加载路由,组件或者模块大部分情况下,你只要做一个莫得感情的 API 工程师调用以上三个 API 就可以解决问题,大幅度降低页面的首次加载体积。但是在前往高级前端工程师的路上,你有可能需要了解其中的原理,(有可能并不需要,数据比原理重要) 来做更加精细化的控制,比如针对缓存。
Code Splitting 的原理是什么?[7]
除了资源体积上的优化,另一个大的优化就是缓存。单页应用有一个最好的方面,就是所有资源都是带有指纹信息的,这意味着所有的资源都是能够设置永久缓存的。
但仅仅如此了吗?
如果你所有的 js 资源都打包成一个文件,它确实有永久缓存的优势。但是当有一行文件进行修改时,这一个大包的指纹信息发生改变,永久缓存失效。
所以我们现在需要做到的是:当修改文件后,造成最小范围的缓存失效,这样便能够更充分的利用缓存,减小宽带,减小服务器费用。一个好消息是 webpack
等打包工具虽然在 optimization
上内置了很多性能优化,但它不会帮你做这件事,它并不知道你有哪些模块,以及这些模块的重要紧急程度,你终于可以大展拳脚了。
此时我们可以对资源进行分层次缓存的打包方案,这是一个建议方案
webpack-runtime
: 应用中的 webpack
的版本比较稳定,分离出来,保证长久的永久缓存react-runtime
: react
的版本更新频次也较低vundor
: 常用的第三方模块打包在一起,如 lodash
,classnames
基本上每个页面都会引用到,但是它们的更新频率会更高一些随着 http2
的发展,特别是多路复用,初始页面的静态资源不受资源数量的影响。因此为了更好的缓存效果以及按需加载,也有很多方案建议把所有的第三方模块进行单模块打包。
在 webpack 中,使用 splitChunks.cacheGroups
{
splitChunks: {
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom "\\/]node_modules[\\/")[\\/]/,
name: 'react',
chunks: 'all'
},
vendor: {
}
}
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
}
毫无疑问在前端中更好地优化打包资源属于网站性能优化强操作性部分的重中之重,整理下本篇文章关于资源优化的所有内
Code Splitting
: 按需加载,优化页面首次加载体积。如根据路由按需加载,根据是否可见按需加载import()
动态加载模块React.lazy()
动态加载组件lodable-component
动态加载路由,组件或者模块Bundle Splitting
:分包,根据模块更改频率分层次打包,充分利用缓存[1]
shfshanyue/blog: https://github.com/shfshanyue/blog
[2]
前端工程化系列: https://github.com/shfshanyue/blog/tree/master/frontend-engineering
[3]
前端高级进阶系列: https://github.com/shfshanyue/blog/tree/master/frontend-engineering
[4]
javascript 代码的体积是如何被压缩的: https://github.com/shfshanyue/blog/blob/master/frontend-engineering/uglify.md
[5]
How to optimize moment.js with webpack: https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
[6]
前端开发中的图片懒加载如何实现: https://q.shanyue.tech/fe/html/1.html
[7]
Code Splitting 的原理是什么?: https://q.shanyue.tech/fe/webpack/206.html