原文链接(格式更好):《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
我们这主要分析Navigation Timing API,记录页面导航到页面 load 加载完毕的一系列事件,其中跟Resource Timing API也有关联
通过window.performance.getEntries()[0]获取,浏览器打印结果如下:
- name:地址栏的值
- entryType:值为
navigation,表明是一个PerformanceNavigationTiming实例 - startTime:开始时间,毫秒
- duration:总耗时,毫秒,从导航开始到 load 加载完的时间,等价于
loadEventEnd - startTime
包含的属性很多,但它们之间是存在联系的,图片来自:Navigation Timing Level 2
流程解读
1. startTime
开始时间,一般为0
2. Process Unload Event
进程解锁事件,一般是执行上一个页面的unload事件(若有),记录两个时间:unloadEventStart/unloadEventEnd
3. Redirect
重定向事件,若有重定向则记录两个时间:redirectStart/redirectEnd
同域名下时,可以直接用该值;若不是同域名,该值不太准确
4. Service Worker Init
初始化service worker,若有启动,则记录启动时间:workerStart
5. Service Worker Fetch Init
初始化service worker 的 fetch,若有启动,则记录启动时间:fetchStart
一般为浏览器开始获取 HTML 的时间
6. HTTP Cache
从缓存里面的找数据,这一步无计时点,但可以通过:domainLookupStart - fetchStart来计算缓存找数据的耗时
7. DNS
开始进行域名的查找,记录两个时间:domianLookupStart/domainLookupEnd
8. TCP
TCP 连接,记录两个时间:connectStart/connectEnd
若是 HTTPS 协议,则额外有安全连接的开始时间: secureConnectStart
connectStart 与 domianLookupStart 之间的差值为:类型判断的耗时,因为需要判断是 HTTP/HTTPS、短链接/长链接 等等
9. Request
发起请求,记录时间:requestStart
10. Early Hints
早期提示,跟 HTTP 的状态码103挂钩,一般告知浏览器一些子资源(JS/CSS等),便于提前加载,可以记录的时间有:interimResponseStart
11. Response
返回响应,记录时间:responseStart/responseEnd
12. 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 树解析完成)
13. 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 的定义为:
- 图像元素的加载: 当
/ 元素加载并成功渲染到屏幕上。
- 背景图像: 如果是通过 CSS 的 background-image 设置的背景图像,当该背景图像被渲染到屏幕上。
- 文本元素: 包含大块内嵌内容的块级元素
LCP 值低的原因:
- 资源问题:
-
- 加载慢:图片/背景等都存在依赖外部资源的情况,所以可以先在
domContentLoadedEvent内进行埋点,看看是否是服务器资源响应慢导致的 - 下载慢:资源很快返回了,但可能由于资源太大/链路太长,导致下载慢
- 加载慢:图片/背景等都存在依赖外部资源的情况,所以可以先在
- 渲染问题:
-
- 渲染被阻断了:一般是 CSS、JavaScript 阻断的
- 单纯渲染慢:可能是客户端硬件的影响
针对性改造:
- 服务器优化
-
- 缓存 HTML 离线页面:将一样的部分/资源进行离线缓存,这样就不需要再通过服务器获取了
- 对图片的优化
-
-
- 不同场景使用不同格式的图片,降低图片大小,加快请求速度
-
-
-
-
- JPEG:有损压缩,网站中的摄影图片、细节丰富的图片等
- PNG:无损压缩,网站图标、LOGO等
- WebP:有损/无损压缩,在支持 WebP 的浏览器中使用
- SVG:矢量图标,图标、LOGO 等
-
-
-
-
- 云资源管理
-
-
- 减少文件大小
-
-
- 去重、压缩、过滤等操作
-
-
-
-
- Webpack、Vite 等工具可提供
-
-
-
-
- CDN - 内容分发网络(Content Delivery Network)
-
-
-
-
- 物理上接近请求点,减少延迟,提高加载速度
-
-
- 客户端优化
-
- 渲染阻断的优化
-
-
- CSS、JS 进行延迟处理
-
-
-
-
- 初次渲染做很多事情并不是很好的,所以可以先用“骨架屏”完成初次渲染,再去写请求数据之类的逻辑,最后填充数据,这样的 LCP 值更低
-
-
-
-
- 首屏优化(单页应用)
-
-
-
-
- 懒加载
-
-
-
-
-
-
- 页面模块、组织模块等
-
-
-
-
-
-
- 异步加载
-
-
-
-
-
-
- 组件本身、样式本身等
-
-
-
-
-
- CSS 模块化
- SSR服务端渲染
-
交互:FID - First Input Delay
FID 的定义:
FID(首次输入延迟):用于衡量用户首次交互到浏览器能够响应的时间
指标:页面的 FID 应该小于 100ms
FID 值低的原因:
- 执行的阻塞
针对性改造:
- 减少 JS 的执行时间,因为 JS 是单线程,执行时会阻塞,大于 50ms 的被称为长任务
-
- 压缩 JS 文件,可以过滤掉多余打印,提升执行效率
- 延迟加载不需要的 JS
-
-
- 模块懒加载
-
-
-
-
- 有些模块在首屏不需要展示时,一开始可以不用去加载
-
-
-
-
- tree shaking
-
-
-
-
- 用于消除 JavaScript 中未引用代码(dead code)的术语。这个过程类似于摇动一棵树,抖落树上的枯叶,只留下需要的部分。
- 最常见的是引入了 xx 库,但只使用了该库一些功能,则打包的时候也应该只打包已使用的功能
-
-
-
- 减少未使用的 polyfill(拦截)
-
-
- 通常是为了兼容低版本的浏览器,而所做的弥补性代码
- 比如:xx 版本浏览器不支持 includes,则可以这样 polyfill
-
// Polyfill for Array.prototype.includes
if(!Array.prototype.includes){
Array.prototype.includes = function(element) {
return this.indexof(element) !== -1
}
}
-
-
- 建议提前做好浏览器版本判断,高版本的话就不走 polyfill 代码逻辑了
-
- 分解耗时任务
-
- 减少执行长的逻辑代码
-
-
- 比如双重数组循环,分成两个循环(虽然性能会差),但阻塞更小
- 比如:表格-先请求列然后再请求数据
-
-
-
-
- 若是请求嵌套:先列请求然后在里面请求数据,最后再赋值渲染表格
- 若是请求串行:先列请求-赋值渲染空表格,加 loading,再请求数据-赋值渲染表格数据,这样的阻塞更小
-
-
-
- Worker
-
-
- 采用 Worker,去分场景承担耗时任务
-
视觉稳定性:CLS - Cumulative Layout Shift
CLS 的定义:
累积布局偏移,衡量页面上元素位置发生变化的频率与程度
指标:页面的 CLS 应该小于 100ms
简单来说就是页面渲染时,元素的位置是否“稳定”
CLS 值低的原因:
- 无尺寸的图片、视频、iframe 等
- 动态内容插入
- 字体的突然改变
针对性改造:
- 不使用无尺寸元素
-
- 图片可以使用:srcset & sizes
-
-
- srcset:描述图片资源与其像素宽度
- sizes:设置图片在不同屏幕下要展示的宽度,默认为 100vw
-
<img
src="1.png"
srcset="1.png 200w,2.png 400w,3.png 800w"
sizes="(max-width: 300px) 200px,
(max-width: 700px) 400px,
(max-width: 1000px) 800px,
100vw
"
/>
<!-- srcset 设置后为:(0,200px],(200px,400px],(400px,800px],(800px,∞], -->
<!-- 当屏幕最大宽度为 300px 时,sizes 使用 200px,对应 srcset 里面的 1.png-->
<!-- 当屏幕最大宽度为 700px 时,sizes 使用 400px,对应 srcset 里面的 2.png-->
<!-- 当屏幕最大宽度为 1000px 时,sizes 使用 800px,对应 srcset 里面的 3.png-->
<!-- 当屏幕最大宽度大于 1000px 时,sizes 使用 100vw(根据屏幕实际宽度),最终对应 srcset 里面的还是 3.png-->
- 整体化内容插入
-
- 相对集中的去完成内容的插入
- 减少动态字体插入
CWV 谷歌浏览器插件:Core Web Vitals Annotations
性能评估- performance
前端性能优化 — 保姆级 Performance 工具使用指南 - 掘金
大厂监控体系
- 建立
-
- 埋点上报
-
-
- 获取关键节点的时间
- 点对点
- 信息采集
-
-
- 数据处理
-
-
- 数据分类
-
-
-
-
- 请求类、渲染类、交互类等等
-
-
-
-
- 阈值设置
- 数据重组()分组
-
-
-
-
- 多维度组装数据,分析对应结果数据的
-
-
-
- 可视化展示
-
-
- 自研
- 开源:grafana(Grafana 中文入门教程)...
-
- 评估
-
- 根据数据指标进行数据圈层()数据归档
- 定位问题
- 修复
-
- 告警通知
- 分派处理
补充知识
Web Worker
定义
基于浏览器的独立线程
特点
- 独立性:与主线程独立,有自己的全局作用域与执行环境
- 无法访问 DOM:没法访问主线程的 DOM,适合做纯计算或数据处理
- 通信:可以与主线程通信
基本使用
注册 Web Worker(A.js)
const worker = new Worker('./web-worker.js')
// 发送消息
worker.postMessage({ data: 'hello web worker'})
定义 Web Worker(web-worker.js)
// 接收消息
self.onmessage = function(event) {
console.log('Message from A.js:', event.data);
}
实际场景之斐波那契数列计算
注册 Web Worker(A.js)
const webWorker = new Worker("./worker/web-worker.js");
webWorker.postMessage({ type: "calculateFibonacci", n: 10 });
webWorker.onmessage = (event) => {
const { type, result } = event.data;
if (type === "calculateFibonacci") {
const calculateFibonacciResult = result;
// do something...
// 结束 Worker
webWorker.terminate();
}
};
定义 Web Worker(web-worker.js)
self.onmessage = function (event) {
const { type, n } = event.data;
if (type === "calculateFibonacci") {
const result = calculateFibonacci(n);
self.postMessage({ type: "calculateFibonacci", result });
}
};
function calculateFibonacci(n) {
if (n <= 1) return n;
else {
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
}
}
Service Worker
定义
基于浏览器,但独立于网页的脚本运行容器。
特点
- 独立性:独立于网页的脚本运行,网页关闭也可运行的。
- 网络代理:可以拦截和处理浏览器的网络请求
- 事件驱动:可以监听浏览器的各种事件
常用于:消息推送通知、离线缓存、拦截处理网络请求
基本使用
注册 Service Worker (A.js)
if('serviceWorker' in navigator){
navigator.serviceWorker.register('./..../service-worker.js')
.then(registration => {
console.log('注册 serviceWorker 成功', registration)
})
.catch(error => {
console.log('注册 serviceWorker 失败', error)
})
}
定义 Service Worker(service-worker.js)
// 监听安装事件,一般用用于设置浏览器的离线缓存
self.addEventListener('install', event => {
// do something ....
})
// 监听 fetch 事件,一般用于拦截和处理请求
self.addEventLinstener('fetch', event => {
// do something ....
})
实际场景之离线缓存
注册 Service Worker
注册代码和基本使用的一致,不累赘
定义 Service Worker
const CACHE_NAME = 'myCache1'
const CACHE_URL = ['/script/main.js', '/script/xxx.js', '/style/xxx.css']
// 监听 Service Worker 的“安装”事件:创建新缓存
self.addEventListener('install', event => {
// waitUntil表示在异步操作之前不要终止
event.waitUntil(
// 创建名为 CACHE_NAME 的缓存版本
caches.open(CACHE_NAME).then(cache => {
// 指定要缓存的地址内容
return cache.addAll(CACHE_URL)
})
)
})
// 监听 Service Worker 的“激活”事件:清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if(cacheName !== CACHE_NAME) {
return caches.delete(cacheName)
}
})
)
})
)
})
// 监听 Service Worker 的“请求”事件:更新缓存
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request)
})
)
})
FCP - First Contentful Paint
首次内容渲染,衡量用户首次看到内容的时间
优化点:异步加载 JS、尽早加载关键资源
TTI - Time to Interactive
可交互时间,衡量页面加载完后到用户可以交互的时间值