随着需求的不断开发,前端项目不断膨胀,业务提出:你们的首页加载也太慢啦,我都需要7、8秒才能看到内容,于是乎主管就让我联合后端开启优化专项,目标是3s内展示完全首页的内容。
性能指标
开启优化时,我们要清晰的知道现状和目标,以及我们采用什么样的手段,通过检测什么指标来查看到优化的过程。
结果指标
根据这个目标,我们可以选择一些性能指标,google 提供了基于用户体验的性能指标,如FCP、LCP、FID、TTI、TBT、CLS等,也有指标更少新的用户体验量化方式 Web Vitals,只选取 LCP、FID、CLS。
我们这次主要选择的指标是 FCP
、LCP
。FCP 表示着用户能最快看到页面内容的时间,LCP 则是可视区域的最大内容,这两个指标代表着用户真实对于页面快和慢的体验,FCP 在 1.8 秒内,LCP 在2.5 秒内是比较好的。
通过 chrome 浏览器 lighthouse 功能可查看的当前页面的结果指标数据,该项目首页 LCP 和 FCP 时间分别为 7.5s、1s,LCP 非常之慢了。
过程指标
FCP
、LCP
是希望达到的结果,在优化的过程中,我们需要一些数据来记录到底在哪些方向做的优化能导致一个比较好的结果,比如请求数量、页面加载时间、打包总体积/入口文件体积、依赖的 cdn 数量、传输资源体积。
无痕模式下通过 chrome 开发者工具 network 记录这些过程指标,当前项目首页过程指标:请求(requests) 117 个,页面加载时间(Load)3.79 秒,打包总体积(resources) 21.8MB、依赖的 CDN(需要自己点击页面右键查看网页源码去数)25个js 、7个css资源,传输资源体积(transferred)6.6MB
较多的资源数量、较大的打包体积都导致了页面加载速度变慢。
页面生命周期
首先我们得知道页面的生命周期,才能有针对性的去对前端参与的过程进行优化。那么从浏览器地址栏输入url,到页面渲染出来,主要经过了哪些步骤呢?
- 输入域名后,DNS 域名解析
- 发起TCP的3次握手
- 建立TCP连接后发起 http 请求
- 服务器端响应http请求,浏览器得到html代码
- 浏览器解析html代码,并请求html代码中的资源
- 浏览器对页面进行渲染呈现给用户(DOM、CSSOM、渲染树)
在这个过程中,有很多网络、服务器参与的过程,而我们主要关注前端能够进行的优化,比如使用 DNS 预解析、缓存机制、减少项目编译后 html / css/ js 包资源大小。
通用优化
删除无用代码
这一步适用于所有项目,定期的清理掉不需要的代码能避免项目无止境的膨胀。因我们项目的历史遗留问题,存在一些无用的cdn资源、依赖、组件、配置,在页面加载的过程中他们占据了一定的网络带宽。
无用 cdn 资源
当前业务场景下这些资源都已经用不到,也无需在页面初始化的时候加载。
功能重复的资源
由不同开发者根据集团规范引入了,两个不同域名但相同功能的前端异常数据采集的 cdn 资源,只需保留一个仍然维护并且推荐使用的即可。
无用依赖
找遍整个项目都用不到的依赖
无用请求逻辑
为了测试组件,跟随首页发送的请求,但因业务场景不太相符,未真正投入使用
无用文件
可用webpack插件 unused-files-webpack-plugin 找到已不需要使用的文件,再通过 nodejs 中内置的文件处理 fs 定义函数将其递归删除。
网络相关
域名收敛
在项目中,我们可能需要加载很多不同的内容,无论是了为了页面更为美观的图片、字体,还是不适合编译到项目中体积较大的cdn的资源(如 echarts、谷歌地图)。
我们知道通过 http 请求获取资源需要经历 DNS 域名解析、TCP 三次握手、建立 TCP 连接后发送 http 请求、服务器响应 … 在这里第一步就是域名解析,如果本地没有缓存,那么这里将会从顶级域名开始一层层往下找,需要耗费很多时间。
通过域名收敛,将相同的应用/资源整合到同一域名,减少不同域名之间 DNS 的解析时间,我们项目资源主要有几大类(xxx代表公司域名)
- JS、CSS 使用
g.alicdn.com
、assets.xxx.cn
- 图片收敛到文件平台
imgcdn.xxx.cn
,assets.xxx.cn
- 字体图标收敛到
at.alicdn.com
- 特殊如高德地图
webapi.amap.com
之类
DNS预解析
在执行 js 文件前会存在一些空闲时间,可以用来解析 dns 地址,dns 预解析是异步执行的,不会对 html 页面代码造成阻塞,这样在真正加载资源时可以减少用户的等待时间。
上面已经将域名进行了收敛,html 中 元素通过 dns- prefetch 的 rel 属性提供这项功能,然后在 href 属性中指定要跨域的域名(仅作用于与当前页面不一样的域名)。
<link rel="dns-prefetch" href= "//imgcdn.xxx.com">
以上两个步骤分别删除无用的资源和提升网络连接,和项目类型、业务场景关联不大,属于很多项目都通用的方案。接下来的步骤需要根据资源编译情况来进行优化。
编译产物优化
在 webpack 配置中增加插件 webpack-analyzer-plugin
,本地运行项目,编译后的资源包情况将在在 8888 端口展示。从编译产物中,我们可以看到每个 js 包原始大小,压缩后尺寸,js中主要有哪些库,根据这些具体的信息进行优化。
图中 vendor.js 是项目打包的入口文件,是第一次打开任意页面都会加载的资源,仔细观察那些体积较大的资源,想办法进行优化。
合并 echarts 版本
从图中可以看到,绘图组件有两个组件库,BizCharts 、echarts,而 echarts 还分为 5.4 和 4.9 的版本,并且在 index.html 中还通过 cdn 引入了 4.2.1 版本并且没有使用 externals排除打包。
找到不同 echarts 版本的组件来源,我们项目中两个版本分别来自 echarts 自身和公司业务组件,还有 react-for-echarts 组件的依赖 。
优化方案是找一个各组件都可以使用版本,将 package.json 和 cdn地址中echarts版本与组件库中保持一致(使用 5.3.3版本),通过 externals 排除打包,这样直接使用 cdn 资源,不再将 echarts 编译至入口文件 vendor.js 中
// index.html
"externals": {
"echarts": "echarts"
}
另外还有 BizCharts,因为使用到的菜单已下线,所以直接将这部分删除。
loadash 工具缩包
项目中常会用到 loadash 之类的工具库,为了使用几个函数将整个工具库的资源都引入非常的不值得,比较好的方式是只通过 import 引入具体需要使用的文件,如 import _add from 'lodash/fp/add'
。
但很有可能并不是项目中的每个人都会遵守这样的规则,如果使用全量引入的方式,如 import _ from 'lodash'
, 导致全量资源被编译至项目主入口文件中。
为了避免这种情况,我们可以使用 babel 插件来 babel-plugin-lodash
将全量引入方式编译成按文件引入。
import _ from 'lodash'
import { add } from 'lodash/fp'
const addOne = add(1)
_.map([1, 2, 3], addOne)
编译成
import _add from 'lodash/fp/add'
import _map from 'lodash/map'
const addOne = _add(1)
_map([1, 2, 3], addOne)
babel-plugin-lodash
是 babel
插件, 在 webpack 中配置到 babel-loader
里
rules: [
{
exclude: /node_modules/,
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['lodash'],
},
}],
},
],
我们项目使用的是集团统一脚手架,只暴露出 webpack-chain 的方式去修改,所以是这样使用。
const merge = require('lodash/merge');
config.module
.rule('jsx')
.use('babel-loader')
.tap((options) => {
return merge(options, {
plugins: ['babel-plugin-lodash'],
});
});
从图中可以看出 xlsx
资源编译后的体积也非常大,但我们项目当前业务场景已没有使用到,所以直接删除,像低频使用的资源,可以替换成 cdn 链接或者拆包编译并延迟引入。
通过以上删除、合并整理,主入口资源从 7.51M 降低 2.51 M。
devtool
借助 chrome 浏览器的 devtool 来进行进行进一步的处理
lighthouse 建议
将项目打开在 chrome 浏览器中,使用 lighthouse 不仅可以用来检测项目 LCP、FCP评分,还有一些针对当前检测页面的建议。
点开之后就是每一条具体的建议,比如提示存在一些首页用不到的 js、css 资源,为图片增加宽高减少布局偏移。
首页用不到但其它地方需要使用 js、css 资源可以去除入口 index.html 的引入,改为到指定文件需要使用时再延迟加载 cdn 资源。
方案大概是这样:
- 如果指定页面没有加载需要的资源,需要通过 scrpt 标签加载需要的 js 资源,link 标签加载需要的 css 资源
- 判断 cdn 引入的资源已挂载在 window 对象之后,再开始执行页面的渲染
这样每个页面负责自己所需要的资源,无需将所有资源的加载压力都放到首页
network 查看接口响应
性能优化也不是前端努力就足够的,页面加载过程中,后端接口响应速度也很关键,如果后端接口响应过慢,前端拿不到数据也无法进行渲染。
在我们项目首页中,获取用户菜单权限的接口非常的慢,每次打开页面都需要一秒钟左右才能响应,存在很严重的阻塞问题,反馈给后端同学进行优化后,能保持在 100-200毫秒完成响应。
优化结果
通过以上优化,性能过程指标,load 时长、资源体积、依赖cdn数量、传输资源体积都有了很大的提升。
性能结果指标 LCP 保持在 1秒,FCP 由 7.5 秒降低至 1.1 秒,页面渲染完成由 8秒 降到 2秒左右,达到了我的预期。
优化完还有非常重要的事情就是 一定要充分测试! 已上线的需求在进行如此大的改动后,一定要先自测一遍业务功能是否受影响,再提给测试同学排期对整个项目进行测试,等到充分测试完成才能发布。
总结
攻城容易守城难,做好了一次优化不意味着能够长期的保持,重要的是全组的同学在平时的需求开发中注意性能做好维护工作,不然资源会像滚雪球一样越来越大,页面的加载速度就在无形之中越来越慢。