性能优化总结

什么是 Web 性能优化
前端性能优化的目的: 让用户访问网站开始到页面完整展示出来的过程中, 通过各种优化策略和优化方法, 让页面加载的更快, 让用户操作响应更加及时, 给用户带来更好的用户体验
为什么需要性能优化
研究表明, 网页性能差直接加速产品的衰败, 也影响网站收入(广告), 因此我们需要提升 Web 性能从而提升用户体验, 公司营收等
优化步骤
以下笔者来详述所有常见的优化手段
一、图片优化
图片分类
图片格式众多, 附上图片分类脑图

图片格式选取
JPEG(JPG/JPE): 有损压缩格式, 不支持透明度, 体积占用不大, 颜色细节质量不高, 颜色丰富, 通常网页大图(bannber / 轮播)等需要使用
PNG(PNG-8/PNG24): 无损压缩格式, 体积占用大, 细节表现好, 通常用于图标 / LOGO 等
GIF: 不支持半透明, 支持全透明, 通常用于动画图标
WEBP: Google 开源的图像格式, 无损的 WEBP 比 PNG 小 26%, 有损 WEBP 比 JPEG 小 25-34%, 比 GIF 有更好的动画, 但是兼容性不好, 需要做 Hack 处理
图片压缩
在实际的使用中, 必须对图片进行压缩, 常用以下工具进行压缩, 可以在本地压缩后上传至 CDN, 也可以在Node
服务端使用在线处理:
- 在线压缩 TinyPng(TinyJpg)
- JPG 压缩工具: Jpegtran
- PNG 压缩工具: node-pngquant-native
- GIF 压缩工具: Gifsicle
响应式图片
不同的网络环境, 应该加载不同尺寸和像素的图片, 通过请求不同的 URL 参数
httP://img.xxx.com/images/q100x100/c2exas....
对应的图片是 100x100
实现方式:
通过 JS 读取窗口大小, 选择合适的图片
通过媒体查询
@media screen and (max-width: 640px) {
.img_640 {
width: 640px;
}
}
- 通过 H5 的新属性
srcset
<img srcset="img-320w" />
逐步加载图片
使用统一占位符
使用
LQIP
(低质量图像占位符)安装:
npm install lqip
, 使用lqip-loader
来引入使用
SQIP
(基于 SVG 的图像占位符)安装:
npm install sqip
图片降级方案
- Web Font 代替图片
- 使用 Data URI 代替图片
- 采用雪碧图(image spriting)
二、HTML 优化
减少 HTML 的嵌套, 减少 DOM 的节点数
压缩 HTML,删除不必要的字符
可以使用构建工具的插件html-webpack-plugin
- HTM 的结构优化
CSS
样式尽量放页面的头部, JS引用放在HTML底部
CSS 加载不会阻塞 DOM Tree 的解析, 但是会阻塞 DOM Tree 的渲染, 也会阻塞后面 JS 的执行。因此 body 元素之前, 可以确保在文档中解析了所有 CSS 样式, 从而减少了浏览器必须重排文档的次数。如果放在底部, 就需要等待最后一个 css 文件下载完成, 出现白屏,影响用户体验
JS 放在底部是防止加载、解析、执行对阻塞页面后续元素的正常渲染
- 设置 favicon.ico
网站不设置 favicon.ico,控制台会报错,设置的优点是更便于用户对品牌的记忆
- 增加网页的骨架屏
三、CSS 优化
- 避免使用通配符和类正则属性选择器
- 避免使用类的多层级和装饰写法:
div#elem.view ul li span{}
- 避免使用占用过多 CPU 和内存的属性:
text-indent: -9999px
- 关注可继承的 CSS 属性, 避免重复定义相同的属性
- 避免使用 table 布局/float 布局, 一个 td 会导致整个回流
- 使用外链 css(CDN 部署),避免使用@import(阻塞 css 文件加载)
- CSS 文件压缩
- 字体部署在 CDN, 或者将字体以 base64 保存在 css 中并通过 localstorage 缓存
- Google 使用国内托管
- CSS 复杂动画应该尽量将该元素脱离文档流, 否则会引起元素以后的所有元素频繁的回流
- 合理开启 GPU 加速(opacity/will-change/transform/filters), 过多的使用会导致内存占用大, 抗锯齿无效
四、JS 优化
JS 代码优化
- JS 文件放在
<body>
底部 - 使用节流和防抖
- 使用事件委托
- 避免使用 eval, 太耗性能
- 避免函数嵌套定义, 会导致多次预编译
- JS 函数的参数类型尽量一致,V8 会调用
turboFan
进行机器码编译优化
JS 的动画优化
- 避免添加大量 JS 动画
- 使用
requestAnimationFrame
代替setTimeout
和setInterval
requestAnimationFrame
告诉浏览器在下次重绘前执行, 而setTimeout
和setInterval
无法保证回调的执行时机
- 动画最好使用 canvas
- 尽量使用 CSS3 动画方案
JS 对 DOM 的操作优化
- 防止频繁的操作 DOM, 尽量批量化操作
- 将 DOM 离线再进行大量操作
- 避免触发同步布局事件
(offset|client|scroll)(Top|Left|Width|Height 的获取都应该缓存起来
五、Webpack 优化
- 依赖包优化(选用相同功能的小库)
- 缩小文件查询时间: (resolve.extension / resolve.mainFields / resolve.modules / resolve.alias)
- Loader 优化(babel 的 cacheDirectory:true / include / exclude / module.noParse)
- HappyPack 多进程打包 / hardSourceWebpackPlugin 设置中间模块缓存 / TerserWebpackPlugin / ingorePlugin / Dll 动态链接库 & DllReference
- TreeShaking / Scope hosting
- 压缩 CSS(optimize-css-assets-webpack-plugin / mini-css-extract-plugin)
- 分包按需加载(splitChunks.cacheGroup)
- long term cache (固化 js 的 chunkhash / 固化 css 的 contenthash / 固化 chunkId(optimize.chunkIds: 'hashd') / 固化 moduleIds(optimize.moduleIds:'name') / 按需加载模块使用魔法字符串'webpackChunkName' / 提取 webpack 的 runtime 代码)
六、使用缓存
- memory cache
- service worker
- disk cache
- push cache(一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放, 同一个 h2 连接可以共享)
七、浏览器的渲染过程
- 浏览器解析 HTML,生成 DOM Tree
- 浏览器解析 CSS, 生成 CSSOM Tree
- 浏览器将 DOM Tree 和 CSSOM Tree 合成渲染树
- 布局: 根据生成的 Render Tree, 进行回流, 计算出每个节点的几何位置
- 绘制: 根据渲染树和回流得到几何信息,得到每个节点的绝对像素,并生成图层
- CPU 将默认图层和复合图层输入到 GPU 进行合成, 最终的到了页面并展示
八、渲染优化
服务端渲染
包括后端同步渲染、同构直出、BigPipe
客户端渲染
JS 渲染:静态化、前后端分离、单页面应用 WebApp: React、 Vue、Angular、PWA 原生 APP: IOS、Android HybridApp: PhoneGap、Appcan 跨平台开发: RN、Flutter、 小程序
预渲染
同构方案集合 CSR 与 SSR 的优点,可以适用于大部分业务场景。但由于在同构的系统架构中,连接前后端的 Node 中间层处于核心链路,系统可用性的瓶颈就依赖于 Node ,一旦作为短板的 Node 挂了,整个服务都不可用
一般的场景,使用预渲染即可, 使用 webpack 插件prerender-spa-plugin
缺点:
- 预渲染只是快照页面, 不适合频繁变动页面
- 设置路由多, 构建时间增长
同构直出
降低首屏渲染时间, 利于 SEO, 直接上线 2 个版本, 利于灾备
- next.js 服务端渲染 React 组件框架
- gatsbyjs: 服务端 React 渲染框架
- nuxt.js 服务端渲染 Vue 框架
关于渲染的技术选型
依赖业务形式: 根据业务情况, 选择最佳的业务方案
依赖团队规模: 创业初期选择同步直出 JSP, 后面团队变大可以使用同构直出
Node server
, 富余人力用 PWA 等等依赖技术水平: 适合公司的技术水平, 选择合适的技术方案
九、加载优化
- 懒加载
对长网页延迟加载特定元素(图片、JS/CSS),也可以是 JS 的特定函数和方法,优点是减少当前屏无效资源的加载
- 预加载
让浏览器预加载某些资源(图片、js、css、模板),提前加载到本地,后面使用直接从缓存中获取,优点是减少用户后续加载资源的等待时间
方式一:
<img src="https://xxxx" style="display:none" />
方式二:
const img = new Image()
img.src = 'https://xxxx'
方式三:
<!-- 当前页需要的资源 as最高优先级,没有as被看做异步请求 -->
<link rel="preload" href="style.css" as="style" />
<!-- 其他页需要的资源 -->
<link rel="prefetch" href="image.png" />
<!-- 预解析跨域的DNS,避免用来解析当前站 -->
<link rel="dns-prefetch" href="https://xxx.com" />
<!-- 预先建立与服务器的连接 -->
<link rel="preconnect" href="https://xxx.com" crossorigin />
- 预渲染
优点:
懒加载组件出来之前, 用户需要时间等待完成; 还有一种预加载的方式是提前渲染, 渲染好后隐藏起来, 用的时候直接展示
实现方式:
<link rel="prerender" href="https://xxx.com" />
- 按需加载
可以分为常规按需加载
(js 或者其他脚本)、不同 App 按需加载(js-sdk 脚本)、 不同设备按需加载(pc 和 h5 样式)、不同分辨率按需加载(css 媒体查询)
十、接口优化
- 接口合并
一个页面很多业务接口和依赖的第三方接口统一起来,在部署在集群的接口上统一调用,减少页面请求
- 接口上 CDN
这是基于接口的性能考虑,把不需要实时更新的接口同步到 CDN,等接口内容变更之后自动同步到 CDN 集群上。如果一定时间内未请求到数据,回源站接口请求
- 接口域名上 CDN
增强可用性,稳定性
- 接口降级
电商大促中,核心接口进行降级备用基础接口进行业务实现。例如推荐接口,大促可以直接用运营的编辑数据。防止接口无法使用时,备用垫底备份数据
- 接口监控
不是指服务端的TP99
,是指用户实际情况成功和失败的情况, 包括弱网、超时、网络异常、网络切换等
- 接口缓存
包括 ajax 缓存、本地缓存(localstorage)、重发请求(网络切换)
十一、WebView 优化
IOS 的 webveiw 分为UIWebview
和WKWebview
, 后者性能更优,内存占用较前者低,加载速度快,可以直接与 JS 互调函数, 而UIWebview
需要第三方库来完善; WKWebview
的缺点是不支持自动注入cookie
,不支持 POST 参数
Android 的系统 webview 分为Webkit Webview
和chromium Webview
(更优秀), 第三方的 webview 主要有X5内核
,速度更快, 兼容性更好, 国内各种手机厂商的碎片化支持更好, 视频播放更加强大
因此 IOS 选用WKWebview
和 Android 使用X5内核
- 使用全局 Webview 优化
APP 启动, 默认不初始化浏览器内核, 当创建实例时才启动内核, 大概有 70-700ms 延迟。客户端刚启动就初始化全局 webview, 需要使用时,直接加载内容;但是额外会消耗一些内存
- URL 预加载
准备和请求页面同步进行,URL load 和动画并行加载
- 滚动条使用体验
模拟 WIFI 下页面加载过程, 让用户感觉变快
- JS-SDK 的优化
一般来说, 常用有三种方式可以调用 nativeApi, 包括上下文注入
、弹窗拦截
、URL Scheme
等
现在可直接使用webkit
直接调用
- H5 离线包方案
首先加载全局包->判断本地是否安装->如果安装了直接解包到内存->如果未安装去线上比对后下载再解包到内存->webview 加载资源时候直接读取内存中的数据->内存中存在直接返回->否则去线上地址取
十一、混合式 APP(RN/Weex)
- RN 的实现方式
技术选型: React 技术全家桶可以选用 RN
- Flutter 的实现方式
)
学习曲线: 相对比 RN 高,重新学习 Dart 语言
性能: Native 性能最好,直接和 Skia(C/C++)
引擎通信,没有 JS Bridge
层
选型建议: 考虑性能, 业务面向多终端, APP 团队人员多
十二、CDN 优化
优点
避免 Ddos 攻击、高可用性处理高流量和负载、节省流量
HTTP 请求流程说明
1、用户在浏览器输入要访问的网站域名,向本地 DNS 发起域名解析请求。
2、域名解析的请求被发往网站授权 DNS 服务器。
3、网站 DNS 服务器解析发现域名已经 CNAME 到了 <www.example.com.c.cdnhwc1.com。>
4、请求被指向 CDN 服务。
5、CDN 对域名进行智能解析,将响应速度最快的 CDN 节点 IP 地址返回给本地 DNS。
6、用户获取响应速度最快的 CDN 节点 IP 地址。
7、浏览器在得到速度最快节点的 IP 地址以后,向 CDN 节点发出访问请求。
8、CDN 节点将用户所需资源返回给用户。
CDN 缓存
常用建议:
- HTML:3 分钟
- JS/CSS: 10 分钟、1 天、30 天
CDN 的Nginx
中通过设置 expires 字段的时长
CDN 灰度发布
不会区域/地区部分运营商优先发布静态资源, 验证通过后, 再进行全量发布
通过设置特殊 VIP 解析至要灰度的城市/运营商
CDN 备战
如果是大促, 增加机房带宽
/ 增加运营商流量
/ CDN 应用缓存时间由 10 分钟设置成 1 小时, 大促后恢复
十三、DNS 优化
DNS 查询
访问远程服务的时候,不会直接使用服务的出口 IP,而是使用域名。DNS 是应用层协议,事实上他是为其他应用层协议工作的,包括不限于 HTTP 和 SMTP 以及 FTP,用于将用户提供的主机名解析为 ip 地址
DNS 获取的流程主要分为以下几个步骤:
浏览器缓存
:当用户通过浏览器访问某域名时,浏览器首先会在自己的缓存中查找是否有该域名对应的 IP 地址(若曾经访问过该域名且没有清空缓存便存在)系统缓存
:当浏览器缓存中无域名对应 IP 则会自动检查用户计算机系统 Hosts 文件 DNS 缓存是否有该域名对应 IP;路由器缓存
:当浏览器及系统缓存中均无域名对应 IP 则进入路由器缓存中检查,以上三步均为客服端的 DNS 缓存;ISP(互联网服务提供商)DNS 缓存
:当在用户客服端查找不到域名对应 IP 地址,则将进入 ISP DNS 缓存中进行查询。比如你用的是电信的网络,则会进入电信的 DNS 缓存服务器中进行查找;根域名服务器
:当以上均未完成,则进入根服务器进行查询。全球仅有 13 台根域名服务器,1 个主根域名服务器,其余 12 为辅根域名服务器。根域名收到请求后会查看区域文件记录,若无则将其管辖范围内顶级域名(如.com)服务器 IP 告诉本地 DNS 服务器;顶级域名服务器
:顶级域名服务器收到请求后查看区域文件记录,若无则将其管辖范围内主域名服务器的 IP 地址告诉本地 DNS 服务器;主域名服务器
:主域名服务器接受到请求后查询自己的缓存,如果没有则进入下一级域名服务器进行查找,并重复该步骤直至找到正确纪录;保存结果至缓存
:本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时将该结果反馈给客户端,客户端通过这个 IP 地址与 web 服务器建立链接。
这里我们需要了解的是
- 首先,DNS 解析流程可能会很长,耗时很高,所以整个 DNS 服务,包括客户端都会有缓存机制,这个作为前端不好涉入;
- 其次,在 DNS 解析上,前端还是可以通过浏览器提供的其他手段来“加速”的。
DNS Prefetch 就是浏览器提供给我们的一个 API。它是 Resource Hint 的一部分。它可以告诉浏览器:过会我就可能要去 yourwebsite.com 上下载一个资源啦,帮我先解析一下域名吧。这样之后用户点击某个按钮,触发了 yourwebsite.com 域名下的远程请求时,就略去了 DNS 解析的步骤。使用方式很简单
<link rel="dns-prefetch" href="//yourwebsite.com" />
DNS 查询优化-预先建立连接
我们知道,建立连接不仅需要 DNS 查询,还需要进行 TCP 协议握手,有些还会有 TLS/SSL 协议,这些都会导致连接的耗时。使用 Preconnect[3] 可以帮助你告诉浏览器:“我有一些资源会用到某个源(origin),你可以帮我预先建立连接”
根据规范,当你使用 Preconnect 时,浏览器大致做了如下处理:
- 首先,解析 Preconnect 的 url;
- 其次,根据当前 link 元素中的属性进行 cors 的设置;
- 然后,默认先将 credential 设为 true,如果 cors 为 Anonymous 并且存在跨域,则将 credential 置为 false;
- 最后,进行连接。
使用 Preconnect 只需要将 rel 属性设为 preconnect 即可:
<link rel="preconnect" href="//sample.com" />
当然,你也可以设置 CORS:
<link rel="preconnect" href="//sample.com" crossorigin />
需要注意的是,标准并没有硬性规定浏览器一定要(而是 SHOULD)完成整个连接过程,与 DNS Prefetch 类似,浏览器可以视情况完成部分工作。
客户端处理
Android 中采用 一些 DNS 模块(okhttp)
- 支持 H2
- 连接池复用减少延迟
- 支持 GZIP,压缩体积
- 响应缓存可以避免网络重复请求
- 配置了多个 IP 地址, 一个 IP 失败,OKhttp 自动尝试下一个
IOS 中可以采用自研 DNS 模块
- APP 启动时,缓存所有可能要用到的域名 IP,同时异步处理,客户端无需缓存
- Cache 中有域名缓存, 直接使用缓存
- 没有缓存则重新向 Http server 申请
Web 前端中的处理, 浏览器有并发数限制,做域名分散,资源分布在多个域名
- Java、php 等 API 接口放在一个域名
- 页面和样式(HTML/JS/CSS)放在一个域名
- 图片(jpg、png、gif)放在一个域名
十四、HTTP 优化
下图是请求声明周期中各个阶段的示意图,可以帮助我们理解发送请求(以及接收响应)的流程。
减少 HTTP 请求数
- css sprites
- 图片使用 DataURI、Web Font
- JS/CSS 文件合并
- JS/CSS 请求 combo
- 接口合并
- 接口存储 localstorage
- 静态资源存储 localstorage
减小 Cookie 大小
- 主站首页设置白名单
- 定期删除非白名单 Cookie
- cookie 设置子域名,防止静态资源挟带 cookie
- 设置合理的过期时长
Cookie 什么时候才会自动携带呢?
NAME=VALUE | 赋予Cookie的名称和其值(必须项) |
---|---|
expires=DATE | Cooke的有效期(若不明确指定则默认为浏览器关闭前为止) |
path=PATH | 将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录) |
domain=域名 | 作为Cookie适用对象的域名(若不指定则默认为创建Cookie的服务器的域名) |
Secure | 仅在HTTPS安全通信时才会发送Cookie |
HttpOnly | 加以限制,使Cookie不能被JavaScript脚本访问 |
如果满足下面几个条件:
1、浏览器端某个 Cookie 的 domain 字段等于 aaa.www.com 或者 <www.com>
2、都是 http 或者 https,或者不同的情况下 Secure 属性为 false
3、要发送请求的路径,即上面的 xxxxx 跟浏览器端 Cookie 的 path 属性必须一致,或者是浏览器端 Cookie 的 path 的子目录,比如浏览器端 Cookie 的 path 为/test,那么 xxxxxxx 必须为/test 或者/test/xxxx 等子目录才可以
Ngix 开启 Gzip 压缩
HTTP 请求头 Accept-Encoding 会将客户端能够理解的内容编码方式——通常是某种压缩算法——进行通知(给服务端)。通过内容协商的方式,服务端会选择一个客户端提议的方式,使用并在响应头 Content-Encoding 中通知客户端该选择。
Nginx 配置: nginx.conf 文件增加 gzip on
Apache 配置: AddOutputFilterByType
和 AddOutputFilter
开启 HTTPS
优点: 利于SEO
和更加安全
实施步骤:
购买证书(GoGetSSL / SSLs.com / SSLmate.com)
本地安装测试证书
// 通过HomeBrew安装
brew install mkcert
// 本地安装根证书
mkcert --insatll
// 本地生成签名
mkcert xxx.com
- 本地 nginx 配置
server{
listen 443 ssl; #启用HTTPS
server_name xxx.com #刚才的签名
ssl_certificate xxx+y.pem;
ssl_certificate_key xxx+y-key.pem;
}
使用 HTTP2
- 二进制传输
- 多路复用 TCP 连接
- header 头部压缩
- 服务端推送
缺点是如果一个TCP包丢失,会导致整个TCP的数据重传
- 升级 OpenSSL
openssl version
- 重新编译
cd nginx-xxx
./configure --with-http_ssl_module --with-http_v2_module
make && make insatll
- 配置 Nginx 的字段
server{
listen 443 ssl http2; #启用http2
server_name xxx.com #刚才的签名
ssl_certificate xxx+y.pem;
ssl_certificate_key xxx+y-key.pem;
}
- 验证 HTTP2
浏览器下查看有没有小绿锁
- 查看浏览器请求的快照 protocol 字段 是不是
h2
十五、性能优化指标
关键性能指标
性能指标 | 含义 | 描述 |
---|---|---|
FP | 首次绘制 | 浏览器首次在屏幕上绘制像素的时间点,即页面开始显示内容的时间。 |
FCP | 首次有内容绘制 | 页面首次绘制出任何文本、图像或其他可视元素的时间点,用户可以看到页面有一些可见的内容, 例如背景图、导航栏或者文字。 |
LCP | 最大内容绘制 | 页面中最大的可见内容元素绘制完成并可见的时间点,通常是页面上最显眼的图像或文本块。 |
TTI | 可交互时间 | 已过期 页面加载完成且用户可以与页面进行交互的时间点,主线程空闲且页面响应用户输入。 |
TBT | 总阻塞时间 | 页面加载过程中,主线程被长时间任务(通常是 JavaScript 执行)阻塞的总时间。 |
CLS | 累计布局偏移 | 页面加载过程中发生的意外布局变化的总量,可能导致用户在交互时误触或出现不良体验。 |
FID | 首次输入延迟 | 已过期,仅支持到2024年9月9日 用户首次与页面交互(如点击按钮)时,页面响应用户输入所需的时间。从用户首次与您的网站互动(即点击链接、点按按钮或使用由 JS 提供支持的自定义控件)到浏览器实际能够对这次互动做出响应的时间。 |
常见性能指标的计算
关键性能指标的计算主要依赖浏览器的渲染流程,通常通过浏览器提供的性能 API(如 PerformanceObserver
、PerformanceEntry
)来获取特定的时间戳。
以下是各个关键性能指标的计算方式
FP(First Paint)- 首次绘制
计算方式
- 使用
PerformanceObserver
监听paint
类型的性能条目,通过entry.name === 'first-paint'
获取FP
时间。 FP
是页面加载时浏览器首次在屏幕上绘制的时间点。
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntriesByName('first-paint')) {
console.log(`FP: ${entry.startTime}`);
}
});
observer.observe({ type: 'paint', buffered: true });
FCP(First Contentful Paint)- 首次内容绘制
首次内容绘制 (FCP) 用于衡量从用户首次导航到网页到网页内容的任何部分在屏幕上呈现的时间。对于此指标,“内容”是指文本、图片(包括背景图片)、<svg>
元素或非白色 <canvas>
元素。
计算方式
- 与
FP
类似,PerformanceObserver
监听paint
类型,通过entry.name === 'first-contentful-paint'
获取FCP
时间 FCP
时间表示页面上第一个内容元素(如文本、图像)的绘制时间- 请务必注意,
FCP
包括前一页面的所有卸载时间、连接设置时间、重定向时间以及首字节时间 (TTFB
)。在现场测量时,这非常重要,可能会导致现场测量结果与实验室测量结果之间出现差异
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntriesByName('first-contentful-paint')) {
console.log(`FCP: ${entry.startTime}`);
}
});
observer.observe({ type: 'paint', buffered: true });
import { onFCP } from 'web-vitals'
// Measure and log FCP as soon as it's available.
onFCP(console.log)
FMP(First Meaningful Paint)- 首次有效绘制
注意
Lighthouse 6.0 中废弃了首次有效绘制 (FMP)。在实践中,FMP 对网页加载的细微差异过于敏感,从而导致结果不一致(双模)。此外,该指标的定义取决于特定于浏览器的实现详情,这意味着该指标无法标准化,无法在所有网络浏览器中实现。今后,不妨考虑改用 Largest Contentful Paint。
计算方式
FMP
目前已经逐步被LCP
替代。一般FMP
计算较为复杂,借助 Lighthouse 等工具会更为准确。FMP
是通过检测页面上主要内容加载完成的时间,通常借助复杂的算法对页面结构进行分析。
LCP(Largest Contentful Paint)- 最大内容绘制
计算方式
- 使用
PerformanceObserver
监听largest-contentful-paint
条目,获取最大内容块的绘制时间点。 - 只需监听该条目中最后一个
entry.startTime
值,即为LCP
时间。 - 开发者不必记住所有这些细微差异,而是可以使用
web-vitals JavaScript
库来衡量LCP
,由其为您处理这些差异(如果可能,请注意 iframe 问题并未涵盖在内)
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(`LCP: ${lastEntry.startTime}`);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
import { onLCP } from 'web-vitals';
// Measure and log LCP as soon as it's available.
onLCP(console.log);
TTI(Time to Interactive)- 可交互时间
注意
事实证明,可交互时间 (TTI) 对离群网络请求和耗时较长的任务过于敏感,导致该指标出现较高的变化。TTI 已作为指标从 Lighthouse 10 中移除。Largest Contentful Paint (LCP)、Total Blocking Time (TBT) 和 Interaction to Next Paint (INP) 等较新的指标通常更适合用来替代 TTI。
计算方式
TTI
是页面的首次可交互时间,通常借助 Lighthouse 等工具进行复杂计算。- 一般定义为页面在
FCP
完成后没有长任务(超过 50ms)阻塞主线程的时间点,且页面已经响应用户输入。
TBT(Total Blocking Time)- 总阻塞时间
给定长任务的阻塞时间是指其超过 50 毫秒的时长。网页的总阻塞时间是在测量的时间范围内(通常是针对网页加载工具的 TTI,或其他工具的总跟踪时间)在 FCP 后发生的每项长任务的阻塞时间的总和。
计算方式
TBT
为所有长任务(超过 50ms 的任务)的阻塞时间总和,计算方式为:(每个长任务的持续时间 - 50ms)
。- 使用
PerformanceObserver
监听longtask
类型,通过entry.duration
检测长任务并累计计算TBT
。
let totalBlockingTime = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const blockingTime = entry.duration - 50;
if (blockingTime > 0) {
totalBlockingTime += blockingTime;
}
}
console.log(`TBT: ${totalBlockingTime}`);
});
observer.observe({ type: 'longtask', buffered: true });
CLS(Cumulative Layout Shift)- 累积布局偏移
计算方式
CLS
是页面加载中发生的非预期布局偏移的累积值。通过PerformanceObserver
监听layout-shift
类型条目,累加entry.value
即可得到CLS
值。只有非用户操作引起的
layout-shift
会计入CLS
,如元素位置变化、图像加载延迟等。开发者无需自己记忆并处理所有这些情况,只需使用
web-vitals JavaScript
库来衡量 CLS,即可考虑上述所有情况(iframe 情况除外)
let cumulativeLayoutShift = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) { // 非用户操作导致的偏移
cumulativeLayoutShift += entry.value;
}
}
console.log(`CLS: ${cumulativeLayoutShift}`);
});
observer.observe({ type: 'layout-shift', buffered: true });
import { onCLS } from 'web-vitals';
// Measure and log CLS in all situations
// where it needs to be reported.
onCLS(console.log);
TTFB(Time To First Byte)- 加载第一个字节所需时间
TTFB 是一个指标(非核心网页指标),用于衡量请求资源到响应第一个字节开始到达之间的时间。一般而言,大多数网站应力求将 TTFB 控制在 0.8 秒或更短。
如上示意图,TTFB 用于衡量 startTime
到 responseStart
之间的经过时间。
TTFB 是以下请求阶段的总和:
- 重定向时间
- Service Worker 启动时间(如果适用)
- DNS 查找
- 连接和 TLS 协商
- 请求,直到收到响应的第一个字节为止
- 缩短连接设置时间和后端延迟时间可以降低 TTFB。
计算方式
new PerformanceObserver((entryList) => {
const [pageNav] = entryList.getEntriesByType('navigation');
console.log(`TTFB: ${pageNav.responseStart}`);
}).observe({
type: 'navigation',
buffered: true
});
import { onTTFB } from 'web-vitals';
// Measure and log TTFB as soon as it's available.
onTTFB(console.log);
TTFB
适用于所有请求,而不仅仅是导航请求。特别是,由于需要设置与这些服务器的连接,托管在跨源服务器上的资源可能会导致延迟。如需衡量该字段中资源的 TTFB,请在 PerformanceObserver 中使用 Resource Timing API
如何检测资源请求的TTFB
呢?
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
for (const entry of entries) {
// Some resources may have a responseStart value of 0, due
// to the resource being cached, or a cross-origin resource
// being served without a Timing-Allow-Origin header set.
if (entry.responseStart > 0) {
console.log(`TTFB: ${entry.responseStart}`, entry.name);
}
}
}).observe({
type: 'resource',
buffered: true
});
FID(First Input Delay)- 首次输入延迟
注意
First Input Delay (FID) 不再是 Core Web Vitals,已被互动到下一次绘制 (INP) 指标取代。因此,Web Core Vitals 于 2024 年 9 月 9 日终止了对 FID 的支持。现在,您应专注于 INP。
FID 衡量的是从用户首次与页面互动(即他们点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器能够实际开始处理事件处理脚本以响应这次互动之间的时间。
为了提供良好的用户体验,网站应尽量将首次输入延迟时间控制在 100 毫秒以内。为确保大多数用户都能达到此目标值,一个合适的衡量阈值是网页加载时间的第 75 个百分位数,并按移动设备和桌面设备进行细分。
计算方式
FID 是一项指标,用于衡量网页在加载期间的响应能力。因此,它仅关注点击、点按和按键等离散操作的输入事件。
滚动和缩放等其他互动是连续操作,具有完全不同的性能限制(此外,浏览器通常能够通过在单独的线程上运行延迟来隐藏延迟)。
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const delay = entry.processingStart - entry.startTime;
console.log('FID candidate:', delay, entry);
}
}).observe({type: 'first-input', buffered: true});
INP(Interaction to Next Paint)- 交互到下一次绘制的时间
INP 与 FID 区别
INP 是 First Input Delay (FID) 的后继指标。虽然这两者都是响应速度指标,但 FID 仅衡量网页上首次互动的输入延迟。INP 通过观察网页上的所有互动(从输入延迟开始,到运行事件处理脚本所需的时间,最后到浏览器绘制下一个帧为止)来改进 FID。
这些差异意味着,INP 和 FID 是不同类型的响应性指标。如果 FID 是旨在评估网页给用户的第一印象的加载响应性指标,则 INP 是更可靠的整体响应指标,而不考虑网页互动发生在网页的生命周期中。
INP 是一项指标,通过观察用户访问网页期间发生的所有点击、点按和键盘互动的延迟时间,评估网页对用户互动的总体响应情况。最终 INP 值是观测到的最长互动时间,离群值会被忽略。INP 的目的不是衡量互动的最终影响(例如网络提取和其他异步操作的界面更新),而是衡量下一次绘制被阻塞的时间。
在左侧,冗长的任务会阻止手风琴打开。这会导致用户多次点击,并认为体验出现问题。当主线程赶上时,它会处理延迟的输入,导致折叠式动作意外打开和关闭。在右侧,响应速度更快的页面可快速打开手风琴,而不会出现任何意外情况。
量化值
- INP 低于或等于 200 毫秒表示网页响应速度良好。
- 如果 INP 高于 200 毫秒或低于 500 毫秒,则表示网页的响应速度需要改进。
- INP 超过 500 毫秒表示网页响应缓慢。
- 良好的 INP 值应不超过 200 毫秒。不良值超过 500 毫秒。
如果您的网站符合纳入 Chrome 用户体验报告 (CrUX) 的条件,您可以通过 PageSpeed Insights 中的 CrUX(以及其他 Core Web Vitals)快速获取 INP 的实测数据。至少,您可以获取网站 INP 的来源级图片,但在某些情况下,您还可以获取网址级数据。
指标计算方式汇总
指标 | 计算方式 | 主要 API |
---|---|---|
FP | paint 条目 first-paint | PerformanceObserver |
FCP | paint 条目 first-contentful-paint | PerformanceObserver |
FMP | 工具辅助计算,已逐步被 LCP 替代 已过期 | Lighthouse |
LCP | largest-contentful-paint 最后一个条目 | PerformanceObserver |
TTI | 工具辅助计算 已过期 | Lighthouse |
TBT | longtask 条目累计阻塞时间 | PerformanceObserver |
CLS | layout-shift 条目累计偏移值 | PerformanceObserver |
TTFB | navigation 条目计算 | PerformanceObserver |
FID | first-input 条目计算差值 已过期 | PerformanceObserver |
这些性能指标的获取可以帮助我们准确分析页面的加载性能,找到渲染瓶颈,以便在优化时有的放矢。
Performance
window.performance
是一个浏览器中用于记录页面加载和解析过程中关键时间点的对象,放置在 global 环境下,通过 JavaScript 可以访问到它。
通过以下代码可以探测和兼容 performance:
const performance =
window.performance || window.msPerformance || window.webkitPerformance
if (performance) {
// 你的代码
}
Performance 总览
部分属性的含义
- memory:显示此刻内存占用情况,是一个动态值
- usedJSHeapSize:JS 对象占用的内存数
- jsHeapSizeLimit:可使用的内存
- totalJSHeapSize:内存大小限制
正常 usedJSHeapSize
不大于 totalJSHeapSize
,如果大于,说明可能出现了内存泄漏。
navigation:显示页面的来源信息
navigation.redirectCount
:表示如果有重定向的话,页面通过几次重定向跳转而来,默认为 0navigation.type
:表示页面打开的方式。0-正常进入;1-通过 window.reload()刷新的页面;2-通过浏览器的前进后退按钮进入的页面;255-非以上方式进入的页面。
onresourcetimingbufferfull:在
resourcetimingbufferfull
事件触发时会被调用的一个event handler
。它的值是一个手动设置的回调函数,这个回调函数会在浏览器的资源时间性能缓冲区满时执行。timeOrigin:一系列时间点的基准点,精确到万分之一毫秒。
timing:一系列关键时间点,包含网络、解析等一系列的时间数据。
Performance Timing
以下是Performance中timing各个时间点
时间点 | 描述 |
---|---|
navigationStart | 同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和 fetchStart 相同。 |
unloadEventStart | 上一个页面 unload 事件抛出时的时间戳。如果没有上一个页面,这个值会返回 0。 |
unloadEventEnd | unloadEventStart 对应的 unload 事件处理完成的时间戳。如果没有上一个页面,这个值会返回 0。 |
redirectStart | 第一个 HTTP 重定向开始的时间戳。如果没有重定向或跨域重定向,这个值会返回 0。 |
redirectEnd | 最后一个 HTTP 重定向完成时的时间戳(即最后一个比特被接收到的时间)。若无重定向或跨域重定向,此值为 0。 |
fetchStart | 浏览器准备好使用 HTTP 请求获取文档的时间戳(会在检查任何应用缓存之前)。 |
domainLookupStart | DNS 域名查询开始的时间戳。若使用持续连接或缓存,此值与 fetchStart 一致。 |
domainLookupEnd | DNS 域名查询完成的时间戳。若使用本地缓存或持久连接,此值与 fetchStart 一致。 |
connectStart | HTTP(TCP)连接建立开始的时间戳。若使用持续连接或缓存,此值与 fetchStart 一致。 |
connectEnd | 浏览器与服务器之间的连接建立完成的时间戳,包含所有握手和认证过程。若为持久连接,则等同于 fetchStart 。 |
secureConnectionStart | HTTPS 连接建立安全握手开始的时间戳。如果当前网页不使用 HTTPS,则返回 0。 |
requestStart | 浏览器向服务器发出 HTTP 请求或读取本地缓存的时间戳。 |
responseStart | 浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。如果连接失败并重开,则取新请求时间。 |
responseEnd | 浏览器从服务器收到(或从本地资源读取)最后一个字节的时间戳。若连接已关闭,则取关闭时的时间戳。 |
domLoading | 网页 DOM 结构开始解析时的时间戳(Document.readyState 变为 loading 时)。 |
domInteractive | 网页 DOM 结构结束解析,开始加载内嵌资源时的时间戳(Document.readyState 变为 interactive )。 |
domContentLoadedEventStart | 解析器发送 DOMContentLoaded 事件,所有需执行的脚本被解析时的时间戳。 |
domContentLoadedEventEnd | 所有需立即执行的脚本被执行完毕时的时间戳。 |
domComplete | 当前文档解析完成(Document.readyState 变为 complete )时的时间戳。 |
loadEventStart | load 事件开始时的时间戳。若事件未发送,则值为 0。 |
loadEventEnd | load 事件完成时的时间戳。若事件未发送或未完成,则值为 0。 |
利用Performance计算网页耗时
INFO
- 重定向耗时:
redirectEnd - redirectStart
- DNS 解析耗时:
domainLookupEnd - domainLookupStart
- TCP 连接耗时:
connectEnd - connectStart
- SSL 安全连接耗时:
connectEnd - secureConnectionStart
- 网络请求耗时(TTFB):
responseStart - requestStart
- 数据传输耗时:
responseEnd - responseStart
- DOM 解析耗时:
domInteractive - responseEnd
- 资源加载耗时:
loadEventStart - domContentLoadedEventEnd
- 首包时间:
responseStart - domainLookupStart
- 首次渲染时间 / 白屏时间:
responseEnd - navigationStart
- 首次可交互时间:
domInteractive - navigationStart
- DOM Ready 时间:
domContentLoadedEventEnd - navigationStart
- 页面完全加载时间:
loadEventStart - navigationStart
performance.timing
记录的是用于分析页面整体性能指标。如果要获取个别资源(例如 JS、图片)的性能指标,就需要使用 Resource Timing API。performance.getEntries()
方法包含了所有静态资源的数组列表, 每一项是一个请求的相关参数有 name
,type
,时间等等。
除了performance.getEntries
之外,performance 还包含一系列有用的方法,比如:
performance.now()
Performance.getEntriesByName()
- ....
在评估页面是否开始渲染方面,首屏时间会比白屏时间更精确,但是二者的结束时间往往很接近。所以要根据自己的业务场景去决定到底该用哪种计算方式。
对于交互性比较少的简单网页,由于加载比较快,所以二者区别不大,因此,可以根据喜好任选一种计算方式。
对于大型的复杂页面,你会发现由于需要处理更多复杂的元素,白屏时间和首屏时间相隔比较远,这时候,计算首屏时间会更有用。
目前白屏常见的优化方案有:
- SSR
- 预渲染
- 骨架屏
优化首屏加载时间的方法:
- CDN 分发(减少传输距离)
- 后端在业务层的缓存
- 静态文件缓存方案
- 前端的资源动态加载
- 减少请求的数量
- 利用好 HTTP 压缩