当我们要衡量分析一个网页的性能时,没有一个确切指标值能直接反映网页整体性能如何。于是,在网页加载的过程中,我们需要抓住各个关键时间点来进行综合分析,这些就是需要了解的性能指标了。
了解了性能指标,接下来就需要借助工具或平台来测量以及上报了;上报后,我们要针对对应问题来使用性能优化方法进行优化。
关于前端性能优化方法,有很多类似的相同的性能优化文章都有介绍了,本文还是会提及并改动,对一些方法谈谈我自己的看法以及公司项目实践的情况。
本文并不对前端性能体系各个地方做非常详尽的解释,只是抛出主要的重点进行抛砖引玉,同时介绍一些性能优化方法的细节点。
性能指标的分类方式各样,如从用户体验的角度可分为文档加载相关的(TTFB、DCL、L),内容呈现相关的(FP、FCP、FMP),交互响应相关的(FID、FSP)
在这就粗略分为两大类处理了,三大核心指标与其它常见的性能指标
当前网站核心 Web 指标指标构成侧重于用户体验的三个方面——加载性能、交互性和视觉稳定性——并包括以下指标(及各指标相应的阈值):
- Largest Contentful Paint (LCP) :最大内容绘制,测量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的2.5 秒内发生。
- First Input Delay (FID) :首次输入延迟,测量交互性。为了提供良好的用户体验,页面的 FID 应为100 毫秒或更短。
- Cumulative Layout Shift (CLS) :累积布局偏移,测量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应保持在 0.1. 或更少。
Largest Contentful Paint 最大内容绘制 (LCP) 代表代表着页面最大元素的渲染时间,通常来说页面中最大元素能快速渲染会让用户感觉页面性能还不错。该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。
最大内容绘制考量的元素类型为:
<img>
元素- 内嵌在
<svg>
元素内的<image>
元素 <video>
元素(使用封面图像)- 通过
url()
函数(而非使用CSS 渐变)加载的带有背景图像的元素 - 包含文本节点或其他行内级文本元素子元素的块级元素
First Input Delay 首次输入延迟 记录用户和页面首次交互操作所花费的时间,即是说用户与页面交互时(如点击链接或按钮),页面可以在多长时间内作出反馈。
FID 指标会影响用户对页面交互性和响应性的第一印象。为了提供良好的用户体验,页面的 FID 应当小于 100 毫秒。
还有一个性能指标 TTI(Time to Interactive)它反映的是用户什么时候可以开始和页面进行交互,但如果用户在 TTI 的时间内,没有与页面产生交互,TTI 是影响不到用户的;而如果想要知道 TTI 对用户的影响,需要 FID 这个指标,不同用户与网页发送交互的时间是不同的,所以对应的 FID 也不同
Cumulative Layout Shift 累积布局偏移 (CLS) 测量整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数,CLS 分数越低,代表页面的布局越稳定。在手机上这个指标更为重要。因为手机屏幕较小,CLS 值大的话会让用户觉得页面体验做的很差。
比如在页面未加载完全时,想要点击页面某个未知,结果页面的广告刚刚加载完动态注入了页面,导致点击的地方被偏移,点到了广告,造成用户交互行为不期望的结果。
CLS 较差的最常见原因为:
- 无尺寸的图像
- 无尺寸的广告、嵌入和 iframe
- 动态注入的内容
- 导致不可见文本闪烁/无样式文本闪烁的网络字体
- 在更新 DOM 之前等待网络响应的操作
关于如何优化 CLS,可以参考该文章:优化 Cumulative Layout Shift 累积布局偏移
- FP(First Paint)首次绘制
FP 指的是页面首次绘制像素的时间(不包括默认背景绘制),即为页面在屏幕上首次发生视觉变化的时间。在 Chrome 的 Performance 面板可看到该指标:
- FCP(First Contentful Paint)首次内容绘制
FCP 指浏览器在页面渲染第一帧内容 DOM(包括任何文本、图片、SVG、非空白 canvas 等)的时间点。
由上述可知,FP 发生的时间一定小于等于 FCP。
如下是官方 Lighthouse 的 FCP 评分的时间区间,如果 FP 和 FCP 能在 1.8 秒内完成就算体验优秀
- FMP(First Meaningful Paint)首次有意义绘制
首次有效绘制,标记主角元素渲染完成的时间点,主角元素可以是视频网站的视频控件,内容网站的页面框架也可以是资源网站的头图等。
目前对 FMP 尚无标准化的定义,因为比较难以用通用的方式确定有效/有意义
的渲染。对于博客网站来说,渲染出标题与首屏文字就是首要内容,对于商城/图片网站来说,渲染图片是首要内容,才是有意义的渲染时间。
比如掘金文章详情页,如果头图的重要性大于标题的话,那它就是有效绘制
以前使用 Lighthouse 工具是有该指标评分的,因为难以标准化,现 Lighthouse 6.0 已弃用,使用了 LCP 指标来替代,具体可见
- DCL(DOMContentLoaded)DOM 解析完成
当 DOMContentLoaded 事件触发时,仅当 DOM 解析完成,不包括样式表,图片等
在文档中没有脚本时,HTML 被完全加载以及解析时,DOMContentLoaded 事件会被触发,无需等待样式表、图片等;如果文档中包含脚本,则脚本会阻塞 HTML 文档的解析。
- L(Onload Event)onload 事件触发
当 onload
事件触发时,代表页面上所有的 DOM、样式表、脚本、图片都已加载完成,即整个页面完全加载完成
关于 DOMContentLoaded 与 Load 的区别可看掘金网站的 performance 指标对应的快照图对比:
为了更形象看出 DOMContentLoaded 与 Load 的区别,可以看这个容易看出区别的[例子] (https://testdrive-archive.azurewebsites.net/HTML5/DOMContentLoaded/Default.html#)
- SI(Speed Index)速度指数
用来衡量页面可见内容填充快慢的指标。该指标涵盖了浏览器何时呈现所有元素,包括不可见的脚本和影响性能的元素;
在用户的角度,它衡量的是用户完整查看页面内容所需的时间,该指标越低越好。
该指标在 Lighthouse 工具可测量。
- TTI(Time To Interactive)首次可交互时间
可交互状态指的是页面上的 UI 组件是可以交互的(比如可以响应按钮的点击等)。通过 TTI 可以让我们了解我们的页面需要多久可以真正达到“可用”的状态,能够快速可靠响应用户操作所需的时间长短。
首次可交互时间,TTI(Time to Interactive)。这个指标计算过程略微复杂,它需要满足以下几个条件:
-
先进行First Contentful Paint 首次内容绘制 (FCP)后开始计算。
-
搜索时长至少为 5 秒的安静窗口。安静窗口的定义为:没有长任务(长任务为执行时间超过 50 ms 的任何任务)且不超过两个正在处理的网络 GET 请求。
- TTFB(Time To First Byte)接收首字节时间
浏览器从请求页面开始到接收第一字节的时间,这个时间段包括 DNS 查找、TCP 连接和 SSL 连接。
TTFB 是由三个主要过程组成的:
- 浏览器发送 HTTP 请求所花费的时间
- 服务器处理该请求所花费的时间
- 服务器将响应的第一个字节发送回浏览器所花费的时间
它衡量的是服务器的响应速度,越快意味着用户等待网站开始加载的时间越少
- TBT(Total Blocking Time)阻塞总时间
TBT 测量页面被阻止响应用户输入(例如鼠标点击、屏幕点击或按下键盘)的总时间。总和是 FCP 和 TTI 之间所有长任务的阻塞部分之和。任何执行时间超过 50 毫秒的任务都是长任务。50 毫秒后的时间量是阻塞部分。例如,如果 Lighthouse 检测到一个 70 毫秒长的任务,则阻塞部分将为 20 毫秒。
- FSP(First Screen Paint)首屏时间
该指标灵活性较大,表示页面从开始加载到首屏内容全部绘制完成的时间,即用户可以看到首屏的全部内容
在了解完性能指标之后,就需要借助一些性能测量工具来获取这些指标的值了
Chrome 开发工具是日常开发过程接触最多的,一般针对性优化时,我们会关注 Network 和 Performance 面板,关于 Performance 面板的简要说明可见我该篇文章:浅析 Chrome Devtools 的 Performance 面板
Lighthouse 用来生成网页的性能评测报告,该工具是性能评分工具中使用频率较高的,安装Lighthouse插件,生成报告查看各指标评分。
建议在无痕浏览器下进行测试,排除其它干扰因素测量出来较为准确,结果如下图:
不仅如此,还能在报告中看到一些可能有用的优化建议:
WebPageTest 用来进行整体的网站质量评估、一站式性能评估。
如下图为官网:
使用步骤:
- 输入要测试的网页网址
- 选择高级配置
- 选择地理位置,从接近你位置最近的测试机器进行测试,并选择 Chrome 浏览器
- 点击 Start Test
结果分析:
首次视图(First View):首次视图的测试,将会把浏览器的缓存和 Cookie 清除,表示访问者第一次访问该网页的情况
请求瀑布图:
点击 Waterfall 显示的瀑布图,可以看到具体的参数详情
上述是通过工具来测量的,假设我们想在 JS 代码中来获取,最简单的方式就是使用集成 Web Vitals 的 JS 库,该项目是由 Chrome 发起的,可获取 CLS、FID、FCP、LCP、TTFB 指标。
使用方法如下:
import { getCLS, getFID, getLCP } from 'web-vitals'
getCLS(console.log)
getFID(console.log)
getLCP(console.log)
此话,还有对应的 Chrome 扩展工具:web-vitals-extension
现在公司谈性能监控,主要就是指用户与真实的网页交互中采集和上报记录性能数据,进行评估。由于我也没做过由 0 到 1 的性能监控平台,这方面也没有太多实战经验,就简单的分享下面一些点。
做性能优化前,一般要不是已经发现了性能瓶颈,要么就是通过指标上报发现什么性能问题,才可以围绕对应性能指标,采取优化手段进行优化。
web-vitals 当收集浏览器端每个用户核心性能指标时,可通过 web-vitals 收集并通过 sendBeacon 上报到打点系统。
很多性能采集库,都基于该对象进行获取计算各种指标,该对象有很多性能相关的时间戳记录:
具体字段解释可见:PerformanceTiming 字段
值得注意的是,在未来该 Performance.timing API 会被废弃,使用 PerformanceNavigationTiming代替
有兴趣了解前端性能监控的可以看下下列这些文章:
当网站所挂服务器离用户越来越远时,访问网站延迟越高。CDN(内容分发网络)就是为了解决这一问题,在多个不同地理位置部署 Web 服务器,根据用户位置分配最近的资源,缩短请求时间达到优化加载速度,降低传输延迟的效果。CDN 域名一般都会缓存到本地中,请求速度也较快。
如果公司没有搭建自己的 CDN,在优化项目想要一些图片/CSS 库/JS 库通过 CDN 引入,免费的 CDN 链接请切记考虑风险,如果 CDN 链接挂了(如果有官方 CDN 链接,通常挂的概率也比较低),你是否有兜底方案,没有请别随意引用免费 CDN。
一个完整的 HTTP 请求需要经历连接与释放过程,需要一定的时间,减少 HTTP 请求能节省一定时间。
单纯说减少 HTTP 请求这个做法在实际工作中是需要权衡的,不是想减少就减少。
在项目中更应该注重的是:减少不必要的 HTTP 请求。比如接口请求后缓存了,发现在一些场景切换中,又重复请求了;或者在一些根本没用到该接口的其它页面,也请求了接口,这时就需要进行减少请求优化。
HTTP1.1 版本存在的问题:线程阻塞,在同一时间,同一域名的请求有一定的数量限制,超过限制数目的请求会被阻塞。
HTTP2 特性:
- 支持二进制传送:解析速度更快
- 支持多路复用:多个请求可以共用一个 TCP 连接,提高了连接的利用率,降低延迟
- 压缩算法压缩头部:减小了传输的体积。且将相同的首部存储起来,仅发送不同的部分,也可以节省流量和加快请求时间
由于 HTTP2 支持多路复用,可并行请求的特性,雪碧图
这个性能优化点也随之过时,现在角度看,雪碧图维护成本也高,基本不会使用了。
为了不让用户每次访问网站都需要重新请求文件,可以通过设置 HTTP 缓存(强缓存/协商缓存)来进行控制,符合缓存条件时直接读取缓存,减少发送请求速度,能提高加载速度。
落到实地,大部分进到公司里面这些都是早配好的。或者这是后端处理的前端没插手机会,如果前端接入 Node 的同学倒可以去搞搞,这一部分就不细说了,有兴趣动手试的可以看我这篇文章:动手搞懂 HTTP 缓存机制
Gzip 通过 LZ77 算法与 Huffman 编码来压缩文件,重复度越高的文件可压缩的空间就越大,对 JS、CSS、HTML 等文本资源均有效。当 Nginx 返回 js 文件的时候,会判断是否开启 gzip,然后压缩后再返回给浏览器。
该压缩方法需要 Nginx 配置 也开启 Gzip 压缩,单纯前端通过 webpack 插件开启 Gzip 压缩是不能达到优化效果的
先不给图片设置 src 路径,当图片出现在浏览器可视区域时,才去加载图片,这就是延迟/懒加载。
图片设置 data-src
属性在页面不可见时图片不会加载。
图片懒加载思路:
- 先在 img 标签设置自定义属性
data-src
- 首屏展示可视区域内的图片(计算对应页面可见时)
src
值替换为data-src
,加载图片
在实际场景中社区已有对应的各种 lazyLoad 库可使用
利用一些在线工具或插件,可以对图片进行压缩,适当的压缩大小通常看不出来区别。
webpack 的话有 image-webpack-loader
,在线网站比如有TinyPNG
点击再或加载到之后再查看清晰/大图
如果有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。
所以,可以用两张图片来进行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图
JavaScript 语言是单线程,所有与浏览器 UI 无关的长时间运行脚本任务只能在一个线程上完成,一次只能做一件事。如果是复杂的计算,则页面运行可能会被阻塞。
此时可以用 Web Worker 进行处理与浏览器 UI 无关的长时间运行脚本。Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。
- 客户端渲染:客户端获取 HTML,下载 JS 文件运行生成 DOM,渲染页面
- 服务端渲染:服务端直接返回 HTML 文件,客户端只需解析 HTML
当前使用 Vue/React 开发的网站,只要不是对 SEO 有要求,大部分都是采用的客户端渲染。而如果网站要追求 SEO 好和首屏速度快的话,那就可采用服务端渲染。因为它只需加载一个渲染完毕的 HTML 文件,比客户端渲染要更快。
不过这种优化近乎重构网站了,通常要求 SEO 的这种一开始项目最好就定好技术栈,中途接入的话,要么重构,要么可以使用改动较小的预渲染。
该种方式是我工作中实践最多的了,打包体积减小可以减少文件下载时间,让用户体验更好。通常会借助 webpack 插件webpack-bundle-analyzer
来进行分析。
在 webpack 可以使用如下插件进行压缩:
- HTML:
html-webpack-plugin
- CSS :
css-minimizer-webpack-plugin
- JavaScript:
terser-webpack-plugin
当页面中有一个文件过大并且还不一定用到的时候,并不需要全部的 JS 模块,这时就可以到触发时才进行导入。这样可以减少首页包体积,加快首页的请求速度。
借助 Webpack 配合 ES6 的按需加载import
方法,对代码进行打包分割,动态导入之后,触发时会加载新的chunk.js
// before
import { toast } from './toastify'
toast('Hello World')
// after
import('./toastify').then((module) => {
module.toast('Hello World')
})
具体可以看下我这篇文章的例子:项目优化实践:Webpack 动态导入 react-toastify
Webpack 打包本身支持 tree shaking,如果进一步优化,可能就得从库本身找起。比如lodash
不支持 Tree Shaking,可以使用lodash-es
来替代
比如使用体积更小的dayjs
来代替moment
库
添加到页面上的事件数量会影响页面的运行性能,如果添加的事件过多,会导致网页的性能下降。采用事件委托的方式,可以大大减少注册事件的个数。
使用事件委托的好处:
- 提高性能
- 节省内存占用,减少事件注册
- 实现当新增子对象时无需再次对其绑定
关于 CSS 选择器没有刻意优化的必要,因为最慢和慢快的选择器性能差别非常小,平时使用只需记住下面几点
- 保持简单,不要使用嵌套过多过于复杂的选择器
- 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用
- 不要使用类选择器和 ID 选择器修饰元素标签,如
h3#markdown-content
,这样多此一举,还会降低效率(CSS 选择器是从右向左匹配的)
了解详细可看该文CSS 渲染原理以及优化策略
在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。
- 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式
- 需要对元素进行复杂操作时,可以先隐藏元素(display:none)操作完成后再显示
- 需要创建多个 DOM 节点时,使用 DocumentFragment 创建完最后再一次性加入文档
当页面有一些事件频繁触发时,为了优化体验,需要对这类事件进行调用次数的限制,于是可以使用防抖与节流来减少调用频率。
- 防抖:一段时间后只执行一次,将多次执行变为最后一次执行
- 节流:在固定的频率执行,将多次执行变为在规定时间内只执行一次
- 优先使用 CSS 来实现动画效果
- 使用 translateZ/translate3d 开启硬件加速
- 合理使用 requestAnimationFrame 代替 setTimeout
如果在视口内列表有较多数据显示(如多达几百上千),如果数据量过大产生过多的 DOM 会造成页面卡顿,此时可以使用虚拟列表,监听视口位置变化,从而对视口内的虚拟列表进行控制。
preload
/prefetch
可控制 HTTP 优先级,从而达到关键请求更快响应的目的。
<link rel="prefetch" href="style.css" as="style" /> <link rel="preload" href="main.js" as="script" />
- preload 优先级较高,提前加载较晚出现,但对当前页面非常重要的资源
- prefetch 优先级较低,提前加载后继路由需要的资源。一般用以加载其它路由资源,如当页面出现 Link,可 prefetch 当前 Link 的路由资源
项目中用到的一些关键库或打包体积较大的开源库,要保持关注,因为这些开源库也会随着版本做优化迭代,因为保持版本最新有必要性。但是,每次大版本升级前,要做好合理评估,如果有重大 bug 或 breaking change,则谨慎起来不要随意升级。
比如从 Webpack4 升级到 Webpack5 版本,就已经做了较多优化了,具体可见升级 Webpack5 实践
开发时在控制台打印了很多信息,这些信息在开发时可以帮助调试和定位。
但如果在控制台打印大量数据会影响页面的整体性能,因此在生产环境建议关闭日志,且可以防止普通用户看到一些敏感日志。
关于该项处理,可以自定义日志打印类,在生产环境默认关闭,但可通过浏览器参数打开;也可以使用 Webpack 插件在线上来进行关闭 console.log 打印。
本文对前端性能体系进行简单的点进行介绍,其中优化手段只介绍了常见的,开发环境的性能优化没有列出以及性能监控部分没有补全.其中单从 Webpack 作为工具的角度,就有很多性能优化点可以列出,这个网上已经有很多文章介绍就不一一列出了。
希望后续能写出一些更好的梳理文章笔记,本文仅供参考。