web前端性能之实践篇
前言
在Web 前端性能优化——了解篇 (opens in a new tab)了解了一些前端性能的知识,本篇文章将主要讲如何去做好性能优化。
在进行性能优化前,我们需要设计好性能监控方案
。
性能监控方案
对于一个 Web 应用而言,监控会分为成 后端监控
和 前端监控
两块去做.
后端监控
也会分为多个模块,比较常见的有 服务器性能监控
、接口性能监控
。
前端监控
一般是针对浏览器访问页面的性能,客户端访问页面的性能主要分为加载、内容呈现和用户交互。如果是 SSR
应用,也需要监控服务器的性能。
如果服务端不是使用 NodeJS,一般不需要前端开发人员去处理监控方案,因此前端一般需要处理的时候 Node.js性能监控
和 浏览器性能监控
。
Node.js 性能监控
Node.js 性能监控也是监控服务器的性能,相关的性能指标介绍可以去看一下这一篇文章:一篇文章带你了解 Node.js 的性能指标 (opens in a new tab)
在 Node.js 可以使用 prom-client (opens in a new tab)进行数据服务器性能采集和上报:
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`
})
}
这只是前端采集和上报的代码,还需要实现的对应的数据分析平台,可以参考官方文档自己搭建一个 基于 Prometheus 的 Grafana 性能分析平台 (opens in a new tab) ,也可以参考这篇文章:使用 Prometheus + Grafana 搭建监控系统 (opens in a new tab)
浏览器性能监控方案
可以根据[Web 前端性能优化——了解篇]介绍的指标计算方法实现自己的 性能监控库,性能数据可以上报到 Grafana 性能分析 平台,实时分析性能数据,也可以直接接入 google 的 firebase (opens in a new tab),不过 firebase 会有一些延迟,但数据会保存三个月。如果不需要自定义性能指标,直接使用 firebase (opens in a new tab) 上报默认的指标即可,firebase 接入文档:https://firebase.google.com/docs/web/setup (opens in a new tab)
性能优化
接下来我们就根据性能指标分类来分析性能优化方案。文档加载相关指标 并不能反映用户体验, 内容呈现指标 基本也包含 文档加载相关指标,因此 文档加载 和 内容呈现 会合并一起进行分析。
加载或内容呈现性能优化
加载性能主要影响因素有: 资源响应速度
、资源体积优化
、 资源加载的顺序
、 代码质量
、 用户网络速度
、 用户设备条件
,不过用户网速和设备我们无法控制,所以我们主要优化方向是其他几个方面
资源响应速度
资源响应速度的主要优化点在于:减少请求数、减少请求资源体积、提升网络传输效率
- 使用 CDN 加速:利用 CDN 增加并发连接和长缓存的优势来加速下载静态资源
- 开启 gzip 压缩:使用 gzip 压缩编码技术,减小资源体积。
- 浏览器缓存:利用浏览器缓存(强缓存与协商缓存)与 Nginx 代理层缓存,缓存静态资源文件。
- 减少网络请求次数和体积:通过压缩文件及合并小文件为大文件,减少网络请求次数,但需要找到合理的平衡点。
- 使用 HTTP/2 :HTTP/2 带来的的优势可以看这一篇文章:解读 HTTP1/HTTP2/HTTP3 (opens in a new tab)
CDN 加速
内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。CDN 的优势可以去看CDN 原理 (opens in a new tab)
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 压缩的区别 (opens in a new tab)
- 之所以在 webpack 的 TerserPlugin 插件已对文件进行压缩的结果下,还进行一次 gzip 压缩,是因为 gzip 能够在已压缩文件的基础上,再次进行压缩
- 之所 webpack 和 nginx 都对静态资源进行 gzip 压缩,是为了让 nginx 能够优先使用静态 gzip 压缩,直接使用 gz 文件的结果作为 gzip 压缩的结果,从而减少实时 gizp 对 cpu 资源的占用
如何开启双端 gzip 压缩,也可以看这里:「简明性能优化」双端开启 Gzip 指南 (opens in a new tab)
浏览器缓存
前端缓存一般可分为 http 缓存和浏览器缓存,http 缓存还分强缓存和协商缓存,如果想对前端缓存了解更多,可以去看一下这本专栏 前端缓存技术与方案解析 (opens in a new tab)
强缓存生产方式:
注意点:浏览器请求资源一般会默认开启强缓存,协商缓存生效的前提是强缓存失效,一般关闭强缓存的方式可以是设置资源响应头:
Cache-Control: max-age=0
协商缓存生效过程:
前面介绍完了强缓存和协商缓存,现在来说一下 HTTP 缓存的常见方案,也算是通用方案:
- 频繁变动的资源,比如 HTML, 关闭强缓存,始终采用协商缓存
- CSS、JS、图片资源等采用强缓存,因为资源请求默认都会采用强缓存,但如何保证有资源有变更的时候不会读取缓存呢,那就是使用 hash 命名,这里就要借助比如类似 webpack 的工具,在打包的时候,根据文件内容来生成文件,并把资源链接动态插入 HTML 中。具体如何如何使用可以去看 webpack 缓存方案 (opens in a new tab)
浏览器缓存按照缓存位置划分可以分为:
- 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 打包策略进行代码拆包,参考 代码分离 (opens in a new tab)
- 图片精灵(升级 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%,那么传输效率肯定能得到大幅提升。
HTTP/2 带来的的优势也可以看这一篇文章:解读 HTTP1/HTTP2/HTTP3 (opens in a new tab)
资源体积优化
不同资源类型优化方式不一样,需要针对各种类型的资源去做响应的优化
文本资源
文本资源包含 HTML
、 CSS
、 JS
等,主要优化手段:
- 代码压缩:minify
- 压缩内容:比如使用 gzip 压缩
- 代码精简
JS
体积优化方案- Tree Shaking
- Code Split
- 组件按需加载
- 代码按需打包
CSS
体积优化方案- 引入第三方库样式文件时按需引入
- 减少不必要的 css 前缀补全
图片资源
一个页面,图片资源的大小一般占据整个页面资源体积的一半以上,因此图片资源体积优化也非常重要。
图片体积优化的手段:
- 去掉不必要的图片,能使用样式实现的不要使用图片
- 雪碧图(HTTP/2 及以上不需要雪碧图)
- 上传图片大小限制
- 压缩项目静态图片
- 接入 Webp 图片处理,可以根据浏览器请求中所带的
accept
来判断是否支持 webp 格式,各 cdn 厂商基本上也都支持 webp 图片转换:阿里云图像处理 (opens in a new tab)
资源加载的顺序
在 从输入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 解析时间,主要主要优化手段有以下几种:
- 减少嵌套循环,使用空间换时间
- 使用高性能算法处理复杂功能
空间复杂度
对性能的影响在于:占据设备内存过大时,可能引起浏览器崩溃等问题,主要主要优化手段:
- 减少全局变量,和注意全局变量所占内存,防止内存不断增大,导致内存溢出。
- 注意销毁不需要的对象,防止不销毁旧的对象,又不断生成新的对象,页面所在内存持续增长,导致页面崩溃。
代码结构设计
好的代码结构设计,对性能的提升影响会特别大,主要的一些设计手段有以下几种:
- 组件懒加载:让页面首次加载只渲染首屏展示的内容,可以提升首屏生产内容的速度和 DOM 解析显示到页面的速度,当 DOM 节点过多,对于浏览器的渲染进程的压力也会越大,显示到页面的时间就会长
virtual-list
: 当长列表页面上拉加载越来越多的内容时,DOM 节点会不断增大,就会造成每次生产新图层会越来越久,就会出现渲染卡顿、内存增大等问题,因此可以使用virtual-list
方案只给 DOM 添加当前屏幕显示的 DOM 节点,可以避免出现渲染卡顿等问题。图片懒加载
:当页面图片太多,同时请求会对服务端造成一定的压力,也可以防止并发加载的资源过多而阻塞 js 或者其他关键资源的加载。- css 对性能的优化方案:可参考仅使用 CSS 提高页面渲染速度 (opens in a new tab)
交互相关性能优化
影响交互性能的主要有几方面: 操作响应速度
、 页面流畅度
、 交互体验设计
。
操作响应速度
什么情况会影响操作的响应速度?
- 操作后执行时间过长,用户等待时间长
- 有任务正在执行,占据主线程,需要等待主线程空闲
我们可以从以下几个方面进行优化操作响应的速度:
- 首次加载只执行首屏需要的代码,非首屏需要的代码可以按需加载
- React 可以开启 Concurrent 模式来实现可中断渲染,优先处理用户操作:Concurrent 模式介绍 (opens in a new tab)
- 当需要执行一段逻辑复杂、耗时较长的代码时,如果不是一定需要阻塞其他应用,可以利用
requestIdleCallback
来在空闲时间再继续执行代码,或者使用异步或者使用定时器让任务在下一个 Eventloop 执行 - 优化代码执行时间
页面流畅度
页面不流畅的主要原因有 两大类
原因:
- 渲染不及时,页面掉帧
- 页面内存占用过高,运行卡顿
渲染不及时,页面掉帧
主要原因:
- 长时间占用 js 线程:js 任务过长
- 页面回流和重绘较多:需要去排查是否有代码会频繁操作
DOM
或者频繁获取offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、clientTop
、clientLeft
、clientWidth
、clientHeight
等需要通过即时计算得到的属性。 - 资源加载阻塞
页面内存占用过高,运行卡顿
主要原因:
- 意外的全局变量引起的内存泄漏
- 闭包引起的内存泄漏
- 被遗忘的定时器
- 循环引用
- DOM 删除时没有解绑事件
- 没有清理的 DOM 元素引用
交互体验设计
交互一般包括:操作、视觉变化、使用引导、用户习惯性行为等等
比如一般我们都会按照 ui 出的设计稿进行编写页面,但是 ui 设计有时候不会注意一些细节,比如弹框显示或隐藏的过渡时间或者效果,友好的过渡效果,不会让用户觉得弹出出现的太突兀。因此我们也许要考虑交互的合理性。
参考资源
- 浏览器工作原理与实践 (opens in a new tab)
- 前端缓存技术与方案解析 (opens in a new tab)
- web.dev (opens in a new tab)
- 前端性能优化指南[7]--Web 性能指标 (opens in a new tab)
- 前端性能优化 24 条建议 (opens in a new tab)
- 聊一聊前端性能优化 (opens in a new tab)
- 仅使用 CSS 提高页面渲染速度 (opens in a new tab)
- 花帽子的博客 (opens in a new tab)
- 一篇文章带你了解 Node.js 的性能指标 (opens in a new tab)
- 在前端开发中,有哪些因素会导致页面卡顿 (opens in a new tab)