Web前端性能优化——实践总结篇
前言
在进行性能优化前,我们需要设计好性能监控方案
。
性能监控方案
对于一个Web应用而言,监控会分为成 后端监控
和 前端监控
两块去做.
后端监控
也会分为多个模块,比较常见的有 服务器性能监控
、接口性能监控
。
前端监控
一般是针对浏览器访问页面的性能,客户端访问页面的性能主要分为加载、内容呈现和用户交互。如果是 SSR
应用,也需要监控服务器的性能。
如果服务端不是使用NodeJS,一般不需要前端开发人员去处理监控方案,因此前端一般需要处理的时候 Node.js性能监控
和 浏览器性能监控
。
Node.js性能监控
在Node.js可以使用 prom-client进行数据服务器性能采集和上报:
function startPromCollect({ jobName }) {
// 引入性能检测
const promClient = require("prom-client");
const address = require("address");
// pushgateway 地址
const pushIp = process.env.BUILD_ENV === "prod" ? "http://xxx" : "http://xxx";
const gateway = new promClient.Pushgateway(pushIp);
// 收集默认数据
promClient.collectDefaultMetrics();
// 30秒push一次
setInterval(() => {
gateway.push({ jobName: jobName, groupings: { instance: address.ip(), pid: process.pid } }, (err, resp, body) => {
if (err) {
console.error(`prometheus Pushgateway pushError: ${err}`);
}
});
}, 30000);
}
// 开发环境不进行上报
if (process.env.BUILD_ENV !== "dev") {
// 开始性能检查
startPromCollect({
jobName: `${process.env.BUILD_ENV}-my-web-app`,
});
}
浏览器性能监控方案
可以根据[Web前端性能优化——了解篇]介绍的指标计算方法实现自己的 性能监控库,性能数据可以上报到 Grafana性能分析 平台,实时分析性能数据,也可以直接接入 google 的 firebase,不过 firebase 会有一些延迟,但数据会保存三个月。如果不需要自定义性能指标,直接使用 firebase 上报默认的指标即可,firebase接入文档:firebase.google.com/docs/web/se…
性能优化
接下来我们就根据性能指标分类来分析性能优化方案。文档加载相关指标 并不能反映用户体验, 内容呈现指标 基本也包含 文档加载相关指标,因此 文档加载 和 内容呈现 会合并一起进行分析。
加载或内容呈现性能优化
加载性能主要影响因素有: 资源响应速度
、资源体积优化
、 资源加载的顺序
、 代码质量
、 用户网络速度
、 用户设备条件
,不过用户网速和设备我们无法控制,所以我们主要优化方向是其他几个方面
资源响应速度
资源响应速度的主要优化点在于:减少请求数、减少请求资源体积、提升网络传输效率
CDN加速
gzip压缩
gzip 编码是改进 web 应用程序性能的技术,通常web 服务器和客户端(浏览器)必须同时支持 gzip。gzip 压缩效率非常高,通常可达 70% 压缩率。
webpack 中可以使用 CompressionWebpackPlugin 插件进行gzip压缩:
if (process.env.NODE_ENV === 'production') {
plugins.push(new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(`\.(${productionGzipExtensions.join('|')})$`),
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false
}))
}
服务端支持 gzip,以 Nginx 配置为例:
gzip on;
gzip_static on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/png image/jpeg image/svg+xml image/gif;
gzip_vary off;
gzip_disable "MSIE [1-6].";
服务器支持gzip压缩后,response header 中会显示 Content-Encoding: gzip。
为何要同时使用webpack gzip压缩和 服务的gzip压缩 呢? 答案可以看一下这里: webpack gzip压缩 和 nginx gzip压缩的区别
- 之所以在 webpack 的 TerserPlugin 插件已对文件进行压缩的结果下,还进行一次 gzip 压缩,是因为 gzip 能够在已压缩文件的基础上,再次进行压缩
- 之所 webpack 和 nginx 都对静态资源进行 gzip 压缩,是为了让 nginx 能够优先使用静态 gzip 压缩,直接使用 gz 文件的结果作为 gzip 压缩的结果,从而减少实时 gizp 对 cpu 资源的占用
浏览器缓存
强缓存生产方式:
注意点:浏览器请求资源一般会默认开启强缓存,协商缓存生效的前提是强缓存失效,一般关闭强缓存的方式可以是设置资源响应头:
Cache-Control: max-age=0
协商缓存生效过程:
前面介绍完了强缓存和协商缓存,现在来说一下 HTTP 缓存的常见方案,也算是通用方案:
- 频繁变动的资源,比如 HTML, 关闭强缓存,始终采用协商缓存
- CSS、JS、图片资源等采用强缓存,因为资源请求默认都会采用强缓存,但如何保证有资源有变更的时候不会读取缓存呢,那就是使用 hash 命名,这里就要借助比如类似webpack的工具,在打包的时候,根据文件内容来生成文件,并把资源链接动态插入HTML中。具体如何如何使用可以去看 webpack缓存方案
浏览器缓存按照缓存位置划分可以分为:
- Service Worker Cache:本质上是一种用 JavaScript 编写的脚本,其作为一个独立的线程,它可以使应用程序能够控制网络请求,缓存这些请求以提高性能,并提供对缓存内容的离线访问。而常见的 Service Worker 应用就是渐进式 Web 应用(Progressive Web Apps)
- Memory Cache 内存缓存:计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
- Disk Cache 磁盘缓存:读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
- Push Cache 推送缓存:HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
http缓存就是利用浏览器的 Memory Cache 和 Disk Cache 两种缓存方式,原理就是浏览器在和服务端进行交互时,添加了一层拦截器,这层拦截器来进行缓存喝是否需要返回缓存内容,也会根据不同情况来判断使用 Memory Cache 还是 Disk Cache 。
减少网络请求次数和体积
- 合理使用 webpack 打包策略进行代码拆包,参考 代码分离
- 图片精灵(升级HTTP/2后不建议使用)
- 清理多余js/css代码
- 图片转base64策略优化,太大的突破不要使用base64,base64体积会更大,且影响js体积
使用HTTP/2
HTTP/2 相对于HTTP/1来说,进行了一些列的增强:
- 多路复用,解决TCP协议慢启动问题:主要因为HTTP/1 每个请求都会新建一个TCP连接,每个域名同时最多同时有6个TCP连接,会造成 TCP 连接之间相互竞争带宽,而且启动TCP连接相对较慢。
- 可以设置请求的优先级:可以在发送请求时,标上该请求的优先级,这样服务器接收到请求之后,会优先处理优先级高的请求。
- 头部压缩:无论是 HTTP/1.1 还是 HTTP/2,它们都有请求头和响应头,这是浏览器和服务器的通信语言。HTTP/2 对请求头和响应头进行了压缩,你可能觉得一个 HTTP 的头文件没有多大,压不压缩可能关系不大,但你这样想一下,在浏览器发送请求的时候,基本上都是发送 HTTP 请求头,很少有请求体的发送,通常情况下页面也有 100 个左右的资源,如果将这 100 个请求头的数据压缩为原来的 20%,那么传输效率肯定能得到大幅提升。
资源体积优化
不同资源类型优化方式不一样,需要针对各种类型的资源去做响应的优化
文本资源
文本资源包含 HTML
、 CSS
、 JS
等,主要优化手段:
- 代码压缩:minify
- 压缩内容:比如使用 gzip 压缩
- 代码精简
JS
体积优化方案- Tree Shaking
- Code Split
- 组件按需加载
- 代码按需打包
CSS
体积优化方案- 引入第三方库样式文件时按需引入
- 减少不必要的 css 前缀补全
图片资源
一个页面,图片资源的大小一般占据整个页面资源体积的一半以上,因此图片资源体积优化也非常重要。
图片体积优化的手段:
- 去掉不必要的图片,能使用样式实现的不要使用图片
- 雪碧图(HTTP/2及以上不需要雪碧图)
- 上传图片大小限制
- 压缩项目静态图片
- 接入Webp图片处理,可以根据浏览器请求中所带的
accept
来判断是否支持webp格式,各cdn厂商基本上也都支持webp图片转换:阿里云图像处理
资源加载的顺序
在 从输入url到页面完全加载
中我们讲解了页面呈现的过程,因此可以知道资源加载是有顺序的,下面来看一下加载阶段的渲染流水线:
可以知道并非所有的资源都会阻塞页面的首次绘制,比如图片、音频、视频等文件就不会阻塞页面的首次渲染;而 JavaScript、首次请求的 HTML 资源文件、CSS 文件是会阻塞首次渲染的,因为在构建 DOM 的过程中需要 HTML 和 JavaScript 文件,在构造渲染树的过程中需要用到 CSS 文件。
我们把这些能阻塞网页首次渲染的资源称为关键资源,因此我们需要梳理清楚哪些是关键资源,哪些是非关键资源。资源加载顺序往往于代码所在位置或者设置的属性有关:
- 一般我们需要把
css
放在header
中,以便于页面渲染出来时,页面能按照预期中的样式正常显示。 js
代码一般放在DOM
底部,如果JavaScript
文件中没有操作DOM
相关代码,就可以将该JavaScript
脚本设置为异步加载,通过async
或defer
来标记代码;async
和defer
虽然都是异步的,不过还有一些差异,使用async
标志的脚本文件一旦加载完成,会立即执行;而使用了defer
标记的脚本文件,需要在DOMContentLoaded
事件之前执行。
代码质量
代码质量分为很多方面,比如代码量、复杂度、代码结构设计等等
代码量
代码量优化的方案主要分为一下几种:
- 代码精简:使用简洁并清晰的代码编写,这个一般与开发者的工作经验或者知识面有很大的关系
- 使用
lodash
提供的功能函数 - 使用正则替代一些复杂的js校验或者匹配功能
- 合理使用一些位运算符
- 使用es6语法
- 去除无效代码
- 使用
- 抽离并封装公用模块代码
- 当一个功能被多次使用就应该封装成公共函数
- 公共组件封装
- css原子化,尽量让每一行css都能得到充分利用
代码复杂度设计
复杂度主要分为 时间复杂度
和 空间复杂度
。
时间复杂度
对性能的影响在于:增加 js 解析时间,主要主要优化手段有以下几种:
- 减少嵌套循环,使用空间换时间
- 使用高性能算法处理复杂功能
空间复杂度
对性能的影响在于:占据设备内存过大时,可能引起浏览器崩溃等问题,主要主要优化手段:
- 减少全局变量,和注意全局变量所占内存,防止内存不断增大,导致内存溢出。
- 注意销毁不需要的对象,防止不销毁旧的对象,又不断生成新的对象,页面所在内存持续增长,导致页面崩溃。
代码结构设计
好的代码结构设计,对性能的提升影响会特别大,主要的一些设计手段有以下几种:
交互相关性能优化
影响交互性能的主要有几方面: 操作响应速度
、 页面流畅度
、 交互体验设计
。
操作响应速度
什么情况会影响操作的响应速度?
- 操作后执行时间过长,用户等待时间长
- 有任务正在执行,占据主线程,需要等待主线程空闲
我们可以从以下几个方面进行优化操作响应的速度:
- 首次加载只执行首屏需要的代码,非首屏需要的代码可以按需加载
- React 可以开启 Concurrent 模式来实现可中断渲染,优先处理用户操作:Concurrent 模式介绍
- 当需要执行一段逻辑复杂、耗时较长的代码时,如果不是一定需要阻塞其他应用,可以利用
requestIdleCallback
来在空闲时间再继续执行代码,或者使用异步或者使用定时器让任务在下一个Eventloop执行 - 优化代码执行时间
页面流畅度
页面不流畅的主要原因有 两大类
原因:
- 渲染不及时,页面掉帧
- 页面内存占用过高,运行卡顿
渲染不及时,页面掉帧
主要原因:
- 长时间占用js线程:js任务过长
- 页面回流和重绘较多:需要去排查是否有代码会频繁操作
DOM
或者频繁获取offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、clientTop
、clientLeft
、clientWidth
、clientHeight
等需要通过即时计算得到的属性。 - 资源加载阻塞
页面内存占用过高,运行卡顿
主要原因:
- 意外的全局变量引起的内存泄漏
- 闭包引起的内存泄漏
- 被遗忘的定时器
- 循环引用
- DOM删除时没有解绑事件
- 没有清理的DOM元素引用
交互体验设计
交互一般包括:操作、视觉变化、使用引导、用户习惯性行为等等
比如一般我们都会按照ui出的设计稿进行编写页面,但是ui设计有时候不会注意一些细节,比如弹框显示或隐藏的过渡时间或者效果,友好的过渡效果,不会让用户觉得弹出出现的太突兀。因此我们也许要考虑交互的合理性。
参考资源
转载自:https://juejin.cn/post/7071975873129218062