1-6、JS 性能优化
指标
Performance Timing API
浏览器的性能指标可以通过 Performance Timing API 来获取,它是一组 API 的集合,常用的有:
- Navigation Timing API:包含了从页面导航开始到页面加载完毕的一系列耗时 API(PerformanceNavigationTiming),通过
window.performance.getEntries()[0]/performance.getEntriesByType('navigation')[0]
获取 - Resource Timing API:包含网页资源(脚本、样式、图片等)加载的耗时 API(PerformanceResourceTiming),通过
window.performance.getEntries()[*]/performance.getEntriesByType('resource')[*]
获取 - Paint Timing API:包含网页绘制相关的耗时 API(PerformancePaintTiming),通过
window.performance.getEntries()[*]/performance.getEntriesByType('paint')[*]
其中window.performance.getEntries()
获取到的是个数组,里面包含一系列 API,并且同时存在多种 API,以下是浏览器打印的结果performance.getEntriesByType('type')
可根据类型获取到对应 API 数组
Navigation Timing API
官方文档:https://www.w3.org/TR/navigation-timing-2/#introduction
我们这主要分析Navigation Timing API
,记录页面导航到页面 load 加载完毕的一系列事件,其中跟Resource Timing API
也有关联
通过window.performance.getEntries()[0]
获取,浏览器打印结果如下:
- name:地址栏的值
- entryType:值为
navigation
,表明是一个PerformanceNavigationTiming
实例 - startTime:开始时间,毫秒
- duration:总耗时,毫秒,从导航开始到 load 加载完的时间,等价于
loadEventEnd - startTime
Navigation Timing Level 2
包含的属性很多,但它们之间是存在联系的,图片来自:Navigation Timing Level 2
流程解读
startTime
开始时间,一般为 0
Process Unload Event
进程解锁事件,一般是执行上一个页面的 unload 事件(若有),记录两个时间:unloadEventStart/unloadEventEnd
Redirect
重定向事件,若有重定向则记录两个时间:redirectStart/redirectEnd
同域名下时,可以直接用该值;若不是同域名,该值不太准确
Service Worker Init
初始化service worker
,若有启动,则记录启动时间:workerStart
Service Worker Fetch Init
初始化service worker 的 fetch
,若有启动,则记录启动时间:fetchStart
一般为浏览器开始获取 HTML 的时间
HTTP Cache
从缓存里面的找数据,这一步无计时点,但可以通过:domainLookupStart - fetchStart
来计算缓存找数据的耗时
DNS
开始进行域名的查找,记录两个时间:domianLookupStart/domainLookupEnd
TCP
TCP 连接,记录两个时间:connectStart/connectEnd
若是 HTTPS 协议,则额外有安全连接的开始时间:secureConnectStart
connectStart 与 domianLookupStart 之间的差值为:类型判断的耗时,因为需要判断是 HTTP/HTTPS、短链接/长链接 等等
Request
发起请求,记录时间:requestStart
Early Hints
早期提示,跟 HTTP 的状态码103
挂钩,一般告知浏览器一些子资源(JS/CSS 等),便于提前加载,可以记录的时间有:interimResponseStart
Response
返回响应,记录时间:responseStart/responseEnd
Processing
参考:Document: readyState property - Web APIs | MDN、Document: DOMContentLoaded event - Web APIs | MDN
处理,一般指的是 HTML、CSS、JS 等资源的加载与解析,记录的时间有:domInteractive
:HTML 加载、解析完成(DOM 树解析完成),但其他资源可能还在加载domContentLoadedEventStart/domContentLoadedEventStart
:HTML 加载、解析完成,并且所有延迟 JS(<script defer src="…">
和<script type="module">
) 已下载并执行时触发domComplete
:HTML 与所有子资源加载完(Render 树解析完成)
Load
参考:Window: load event - Web APIs | MDN
HTML 与所有子资源加载完后触发,记录两个时间:loadEventStart/loadEventEnd
实际运用
指标解读:
- Total:总时间,各项指标之合
- DNS:
domainLookupEnd - domainLookupStart
,DNS 查询花费的时间 - TCP:
connectEnd - connectStart
,TCP 建立连接花费的时间 - Request:
responseStart - requestStart
,请求到响应花费的时间 - Response:
responseEnd - responseStart
,接受响应花费的时间 - Processing:
domComplete - domInteractive
,渲染页面花费的时间 - Load:
loadEventEnd - loadEventStart
,load 阶段花费的时间
CWV:Core Web Vitals - 核心 Web 指标(谷歌)
谷歌提出的,从:加载、交互、视觉稳定性三个方面衡量
加载:LCP - Largest Contentful Paint
LCP(最大内容渲染) 应该在页面首次加载后 2.5s 内发生,白话:在前 2.5s 内完成最大内容的渲染
LCP 的定义为
- 图像元素的加载: 当
<img>/<svg>
元素加载并成功渲染到屏幕上。 - 背景图像: 如果是通过 CSS 的 background-image 设置的背景图像,当该背景图像被渲染到屏幕上。
- 文本元素: 包含大块内嵌内容的块级元素
LCP 的计算原理
浏览器有个事件,在每次渲染元素时,去找到当前“渲染面积”最大的元素计算渲染它的耗时,所以浏览器在渲染时,“渲染面积”最大的元素会一直变化。
可以通过 PerformanceObserver API 监听 largest-contentful-paint 事件来获取 LCP 值:
1 |
|
如何计算 LCP 指标–应用性能监控全链路版-火山引擎
可以使用:web-vitals 库获取具体 LCP 值
LCP 值低的原因
- 资源问题:
- 加载慢:图片/背景等都存在依赖外部资源的情况,所以可以先在
domContentLoadedEvent
内进行埋点,看看是否是服务器资源响应慢导致的 - 下载慢:资源很快返回了,但可能由于资源太大/链路太长,导致下载慢
- 加载慢:图片/背景等都存在依赖外部资源的情况,所以可以先在
- 渲染问题:
- 渲染被阻断了:一般是 CSS、JavaScript 阻断的
- 单纯渲染慢:可能是客户端硬件的影响
针对性改造
- 服务器优化
- 缓存 HTML 离线页面:将一样的部分/资源进行离线缓存,这样就不需要再通过服务器获取了
- 对图片的优化
- 不同场景使用不同格式的图片,降低图片大小,加快请求速度
- JPEG:有损压缩,网站中的摄影图片、细节丰富的图片等
- PNG:无损压缩,网站图标、LOGO 等
- WebP:有损/无损压缩,在支持 WebP 的浏览器中使用
- SVG:矢量图标,图标、LOGO 等
- 云资源管理
- 不同场景使用不同格式的图片,降低图片大小,加快请求速度
- 减少文件大小
- 去重、压缩、过滤等操作
- Webpack、Vite 等工具可提供
- CDN - 内容分发网络(Content Delivery Network)
- 物理上接近请求点,减少延迟,提高加载速度
- 去重、压缩、过滤等操作
- 客户端优化
- 渲染阻断的优化
- CSS、JS 进行延迟处理
- 初次渲染做很多事情并不是很好的,所以可以先用“骨架屏”完成初次渲染,再去写请求数据之类的逻辑,最后填充数据,这样的 LCP 值更低
- 首屏优化(单页应用)
- 懒加载
- 页面模块、组织模块等
- 异步加载
- 组件本身、样式本身等
- 懒加载
- CSS 模块化
- SSR 服务端渲染
- CSS、JS 进行延迟处理
- 渲染阻断的优化
交互:FID - First Input Delay
FID 的定义
FID(首次输入延迟):用于衡量用户首次交互到浏览器能够响应的时间
指标:页面的 FID 应该小于 100ms
FID 的计算原理
FID 发生在 FCP 和 TTI 之间。
可以通过 PerformanceObserver API 监听 first-input 事件来获取 FID 值:
1 |
|
如何计算 FID 和 MPFID 指标–应用性能监控全链路版-火山引擎
可以使用:web-vitals 库获取具体 FID 值
FID 值低的原因
- 执行的阻塞
针对性改造
- 减少 JS 的执行时间,因为 JS 是单线程,执行时会阻塞,大于 50ms 的被称为长任务
- 压缩 JS 文件,可以过滤掉多余打印,提升执行效率
- 延迟加载不需要的 JS
- 模块懒加载
- 有些模块在首屏不需要展示时,一开始可以不用去加载
- tree shaking
- 用于消除 JavaScript 中未引用代码(dead code)的术语。这个过程类似于摇动一棵树,抖落树上的枯叶,只留下需要的部分。
- 最常见的是引入了 xx 库,但只使用了该库一些功能,则打包的时候也应该只打包已使用的功能
- 模块懒加载
- 减少未使用的 polyfill(拦截)
- 通常是为了兼容低版本的浏览器,而所做的弥补性代码
- 比如:xx 版本浏览器不支持 includes,则可以这样 polyfill
1 |
|
plain - 建议提前做好浏览器版本判断,高版本的话就不走 polyfill 代码逻辑了
- 分解耗时任务
- 减少执行长的逻辑代码
- 比如双重数组循环,分成两个循环(虽然性能会差),但阻塞更小
- 比如:表格-先请求列然后再请求数据
- 若是请求嵌套:先列请求然后在里面请求数据,最后再赋值渲染表格
- 若是请求串行:先列请求-赋值渲染空表格,加 loading,再请求数据-赋值渲染表格数据,这样的阻塞更小
- Worker
- 采用 Worker,去分场景承担耗时任务
- 减少执行长的逻辑代码
视觉稳定性:CLS - Cumulative Layout Shift
CLS 的定义
累积布局偏移,衡量页面上元素位置发生变化的频率与程度
指标:页面的 CLS 应该小于 0.1
简单来说就是页面渲染时,元素的位置是否“稳定”
CLS 的计算原理
可以通过 PerformanceObserver API 监听 layout-shift 事件来获取 FID 值:
1 |
|
如何计算 CLS 指标–应用性能监控全链路版-火山引擎
可以使用:web-vitals 库获取具体 CLS 值
CLS 值低的原因
- 无尺寸的图片、视频、iframe 等
- 动态内容插入
- 字体的突然改变
针对性改造
- 不使用无尺寸元素
- 图片可以使用:srcset & sizes
- srcset:描述图片资源与其像素宽度
- sizes:设置图片在不同屏幕下要展示的宽度,默认为 100vw
- 图片可以使用:srcset & sizes
1 |
|
- 整体化内容插入
- 相对集中的去完成内容的插入
- 减少动态字体插入
CWV 谷歌浏览器插件:Core Web Vitals Annotations
性能评估- performance
前端性能优化 — 保姆级 Performance 工具使用指南 - 掘金
大厂监控体系
- 建立
- 埋点上报
- 获取关键节点的时间
- 点对点
- 信息采集
- 数据处理
- 数据分类
- 请求类、渲染类、交互类等等
- 阈值设置
- 数据重组/分组
- 多维度组装数据,分析对应结果数据的
- 数据分类
- 可视化展示
- 自研
- 开源:grafana(Grafana 中文入门教程)…
- 埋点上报
- 评估
- 根据数据指标进行数据圈层/数据归档
- 定位问题
- 修复
- 告警通知
- 分派处理
补充知识
Web Worker
定义
基于浏览器的独立线程
特点
- 独立性:与主线程独立,有自己的全局作用域与执行环境
- 无法访问 DOM:没法访问主线程的 DOM,适合做纯计算或数据处理
- 通信:可以与主线程通信
基本使用
注册 Web Worker(A.js)
1 |
|
定义 Web Worker(web-worker.js)
1 |
|
实际场景之斐波那契数列计算
注册 Web Worker(A.js)
1 |
|
定义 Web Worker(web-worker.js)
1 |
|
Service Worker
定义
基于浏览器,但独立于网页的脚本运行容器。
特点
- 独立性:独立于网页的脚本运行,网页关闭也可运行的。
- 网络代理:可以拦截和处理浏览器的网络请求
- 事件驱动:可以监听浏览器的各种事件
常用于:消息推送通知、离线缓存、拦截处理网络请求
基本使用
注册 Service Worker (A.js)
1 |
|
定义 Service Worker(service-worker.js)
1 |
|
实际场景之离线缓存
注册 Service Worker
注册代码和基本使用的一致,不累赘
定义 Service Worker
1 |
|
FP - First Paint
首次渲染,衡量白屏的时间
Performance 直接获取:
如何计算 FP 和 FCP 指标–应用性能监控全链路版-火山引擎
FCP - First Contentful Paint
首次内容渲染,衡量用户首次看到内容的时间
优化点:异步加载 JS、尽早加载关键资源
Performance 直接获取:
也可以使用:web-vitals 库获取具体 FCP 值
如何计算 FP 和 FCP 指标–应用性能监控全链路版-火山引擎
FMP - First Meaningful Paint
首次有效绘制,衡量用户首次看到“有效内容”的时间
无标准的计算方法,一般用 LCP 代替
一种计算方式:
DOM 结构变化的时间点可以利用 MutationObserver API 来获得。
通过 MutationObserver 监听每一次页面整体的 DOM 变化,触发 MutationObserver 的回调,在回调计算出当前 DOM 树的分数,分数变化最剧烈的时刻,即为 FMP 的时间点。
TTI - Time to Interactive
可交互时间,衡量页面加载完后到用户可以交互的时间值
计算方式:
参考上述示意图(图中的 First Consistently Interactive 即为 TTI )。
- 从起始点(一般选择 FCP 或 FMP)时间开始,向前搜索一个不小于 5s 的静默窗口期。静默窗口期:窗口所对应的时间内没有 Long Task,且进行中的网络请求数不超过 2 个。
- 找到静默窗口期后,从静默窗口期向后搜索到最近的一个 Long Task,Long Task 的结束时间即为 TTI。
- 如果没有找到 Long Task,以起始点时间作为 TTI。
- 如果 2、3 步骤得到的 TTI < DOMContentLoadedEventEnd,以 DOMContentLoadedEventEnd 作为 TTI
TBT - Total Blocking Time
总阻塞时间,衡量从 FCP 到 TTI 之间主线程被阻塞的总时间
大于 50ms 的任务被称为长任务。
TBT 计算的就是每个任务时长 - 50ms 后,剩余的总和
TTFT - Time to First Byte
首次字节时间,衡量从浏览器发送请求到接收到服务器响应的第一个字节所需的时间
计算方式:
1 |
|
可以使用:web-vitals 库获取具体 TTFT 值
参考资料
最全的前端性能定位总结 - 掘金
如何根据页面的 timing 指标计算出各阶段值–应用性能监控全链路版-火山引擎
Google Web Vitals