1-6、JS 性能优化

指标

Performance Timing API


浏览器的性能指标可以通过 Performance Timing API 来获取,它是一组 API 的集合,常用的有:

  1. Navigation Timing API:包含了从页面导航开始到页面加载完毕的一系列耗时 API(PerformanceNavigationTiming),通过window.performance.getEntries()[0]/performance.getEntriesByType('navigation')[0]获取
  2. Resource Timing API:包含网页资源(脚本、样式、图片等)加载的耗时 API(PerformanceResourceTiming),通过window.performance.getEntries()[*]/performance.getEntriesByType('resource')[*]获取
  3. Paint Timing API:包含网页绘制相关的耗时 API(PerformancePaintTiming),通过window.performance.getEntries()[*]/performance.getEntriesByType('paint')[*]

其中window.performance.getEntries()获取到的是个数组,里面包含一系列 API,并且同时存在多种 API,以下是浏览器打印的结果

performance.getEntriesByType('type') 可根据类型获取到对应 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
timestamp-diagram.svg

流程解读

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 | MDNDocument: 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 的定义为

  1. 图像元素的加载: 当<img>/<svg>元素加载并成功渲染到屏幕上。
  2. 背景图像: 如果是通过 CSS 的 background-image 设置的背景图像,当该背景图像被渲染到屏幕上。
  3. 文本元素: 包含大块内嵌内容的块级元素

LCP 的计算原理

浏览器有个事件,在每次渲染元素时,去找到当前“渲染面积”最大的元素计算渲染它的耗时,所以浏览器在渲染时,“渲染面积”最大的元素会一直变化。
可以通过 PerformanceObserver API 监听 largest-contentful-paint 事件来获取 LCP 值:

1
2
3
4
5
6
7
8
9
10
11
12
if ('performance' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.startTime, 'ms');
// 如果需要记录或上报 LCP 时间,则在此处处理
}
}
});

observer.observe({ type: 'largest-contentful-paint', buffered: true });
}

如何计算 LCP 指标–应用性能监控全链路版-火山引擎
可以使用:web-vitals 库获取具体 LCP 值

LCP 值低的原因

  1. 资源问题:
    1. 加载慢:图片/背景等都存在依赖外部资源的情况,所以可以先在domContentLoadedEvent内进行埋点,看看是否是服务器资源响应慢导致的
    2. 下载慢:资源很快返回了,但可能由于资源太大/链路太长,导致下载慢
  2. 渲染问题:
    1. 渲染被阻断了:一般是 CSS、JavaScript 阻断的
    2. 单纯渲染慢:可能是客户端硬件的影响

针对性改造

  1. 服务器优化
    1. 缓存 HTML 离线页面:将一样的部分/资源进行离线缓存,这样就不需要再通过服务器获取了
    2. 对图片的优化
      1. 不同场景使用不同格式的图片,降低图片大小,加快请求速度
        1. JPEG:有损压缩,网站中的摄影图片、细节丰富的图片等
        2. PNG:无损压缩,网站图标、LOGO 等
        3. WebP:有损/无损压缩,在支持 WebP 的浏览器中使用
        4. SVG:矢量图标,图标、LOGO 等
      2. 云资源管理
    3. 减少文件大小
      1. 去重、压缩、过滤等操作
        1. Webpack、Vite 等工具可提供
      2. CDN - 内容分发网络(Content Delivery Network)
        1. 物理上接近请求点,减少延迟,提高加载速度
  2. 客户端优化
    1. 渲染阻断的优化
      1. CSS、JS 进行延迟处理
        1. 初次渲染做很多事情并不是很好的,所以可以先用“骨架屏”完成初次渲染,再去写请求数据之类的逻辑,最后填充数据,这样的 LCP 值更低
      2. 首屏优化(单页应用)
        1. 懒加载
          1. 页面模块、组织模块等
        2. 异步加载
          1. 组件本身、样式本身等
      3. CSS 模块化
      4. SSR 服务端渲染

交互:FID - First Input Delay

FID 的定义

FID(首次输入延迟):用于衡量用户首次交互到浏览器能够响应的时间
指标:页面的 FID 应该小于 100ms

FID 的计算原理

FID 发生在 FCP 和 TTI 之间。
可以通过 PerformanceObserver API 监听 first-input 事件来获取 FID 值:

1
2
3
4
5
6
7
8
9
10
if ('performance' in window && 'getEntriesByType' in performance) {
const entries = performance.getEntriesByType('event');
for (const entry of entries) {
if (entry.entryType === 'first-input') {
// 这里的 entry.duration 就是 FID 值(以毫秒为单位)
console.log(`First Input Delay: ${entry.duration} ms`);
break; // 只考虑第一个交互事件
}
}
}

如何计算 FID 和 MPFID 指标–应用性能监控全链路版-火山引擎
可以使用:web-vitals 库获取具体 FID 值

FID 值低的原因

  • 执行的阻塞

针对性改造

  • 减少 JS 的执行时间,因为 JS 是单线程,执行时会阻塞,大于 50ms 的被称为长任务
    • 压缩 JS 文件,可以过滤掉多余打印,提升执行效率
    • 延迟加载不需要的 JS
      • 模块懒加载
        • 有些模块在首屏不需要展示时,一开始可以不用去加载
      • tree shaking
        • 用于消除 JavaScript 中未引用代码(dead code)的术语。这个过程类似于摇动一棵树,抖落树上的枯叶,只留下需要的部分。
        • 最常见的是引入了 xx 库,但只使用了该库一些功能,则打包的时候也应该只打包已使用的功能
    • 减少未使用的 polyfill(拦截)
      • 通常是为了兼容低版本的浏览器,而所做的弥补性代码
      • 比如:xx 版本浏览器不支持 includes,则可以这样 polyfill
1
2
3
4
5
6
// Polyfill for Array.prototype.includes
if(!Array.prototype.includes){
Array.prototype.includes = function(element) {
return this.indexof(element) !== -1
}
}

plain - 建议提前做好浏览器版本判断,高版本的话就不走 polyfill 代码逻辑了

  • 分解耗时任务
    • 减少执行长的逻辑代码
      • 比如双重数组循环,分成两个循环(虽然性能会差),但阻塞更小
      • 比如:表格-先请求列然后再请求数据
        • 若是请求嵌套:先列请求然后在里面请求数据,最后再赋值渲染表格
        • 若是请求串行:先列请求-赋值渲染空表格,加 loading,再请求数据-赋值渲染表格数据,这样的阻塞更小
    • Worker
      • 采用 Worker,去分场景承担耗时任务

视觉稳定性:CLS - Cumulative Layout Shift

CLS 的定义

累积布局偏移,衡量页面上元素位置发生变化的频率与程度
指标:页面的 CLS 应该小于 0.1
简单来说就是页面渲染时,元素的位置是否“稳定”

CLS 的计算原理

可以通过 PerformanceObserver API 监听 layout-shift 事件来获取 FID 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let clsValue = 0;
let clsEntries = [];
let sessionValue = 0;
let sessionEntries = [];
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// If the entry occurred less than 1 second after the previous entry and
// less than 5 seconds after the first entry in the session, include the
// entry in the current session. Otherwise, start a new session.
if (sessionValue
&& entry.startTime - lastSessionEntry.startTime < 1000
&& entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// If the current session value is larger than the current CLS value
// update CLS and the entries contributing to it.
if (sessionValue > clsValue) {
clsValue = sessionValue;
clsEntries = sessionEntries;
// Log the updated value (and its entries) to the console.
console.log('CLS:', clsValue, clsEntries)
}
}
}}).observe({type: 'layout-shift', buffered: true});

如何计算 CLS 指标–应用性能监控全链路版-火山引擎
可以使用:web-vitals 库获取具体 CLS 值

CLS 值低的原因

  • 无尺寸的图片、视频、iframe 等
  • 动态内容插入
  • 字体的突然改变

针对性改造

  • 不使用无尺寸元素
    • 图片可以使用:srcset & sizes
      • srcset:描述图片资源与其像素宽度
      • sizes:设置图片在不同屏幕下要展示的宽度,默认为 100vw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 工具使用指南 - 掘金

大厂监控体系

  1. 建立
    1. 埋点上报
      1. 获取关键节点的时间
      2. 点对点
      3. 信息采集
    2. 数据处理
      1. 数据分类
        1. 请求类、渲染类、交互类等等
      2. 阈值设置
      3. 数据重组/分组
        1. 多维度组装数据,分析对应结果数据的
    3. 可视化展示
      1. 自研
      2. 开源:grafana(Grafana 中文入门教程)…grafana
  2. 评估
    1. 根据数据指标进行数据圈层/数据归档
    2. 定位问题
  3. 修复
    1. 告警通知
    2. 分派处理

补充知识

Web Worker

定义

基于浏览器的独立线程

特点

  1. 独立性:与主线程独立,有自己的全局作用域与执行环境
  2. 无法访问 DOM:没法访问主线程的 DOM,适合做纯计算或数据处理
  3. 通信:可以与主线程通信

基本使用

注册 Web Worker(A.js)

1
2
3
4
const worker = new Worker('./web-worker.js')

// 发送消息
worker.postMessage({ data: 'hello web worker'})

定义 Web Worker(web-worker.js)

1
2
3
4
// 接收消息
self.onmessage = function(event) {
console.log('Message from A.js:', event.data);
}

实际场景之斐波那契数列计算

注册 Web Worker(A.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

定义

基于浏览器,但独立于网页的脚本运行容器。

特点

  1. 独立性:独立于网页的脚本运行,网页关闭也可运行的。
  2. 网络代理:可以拦截和处理浏览器的网络请求
  3. 事件驱动:可以监听浏览器的各种事件

常用于:消息推送通知、离线缓存、拦截处理网络请求

基本使用

注册 Service Worker (A.js)

1
2
3
4
5
6
7
8
9
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)

1
2
3
4
5
6
7
8
9
// 监听安装事件,一般用用于设置浏览器的离线缓存
self.addEventListener('install', event => {
// do something ....
})

// 监听 fetch 事件,一般用于拦截和处理请求
self.addEventLinstener('fetch', event => {
// do something ....
})

实际场景之离线缓存

注册 Service Worker

注册代码和基本使用的一致,不累赘

定义 Service Worker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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)
})
)
})

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 的时间点。

如何计算 FMP 指标–应用性能监控全链路版-火山引擎

TTI - Time to Interactive

可交互时间,衡量页面加载完后到用户可以交互的时间值
计算方式:

参考上述示意图(图中的 First Consistently Interactive 即为 TTI )。

  1. 从起始点(一般选择 FCP 或 FMP)时间开始,向前搜索一个不小于 5s 的静默窗口期。静默窗口期:窗口所对应的时间内没有 Long Task,且进行中的网络请求数不超过 2 个。
  2. 找到静默窗口期后,从静默窗口期向后搜索到最近的一个 Long Task,Long Task 的结束时间即为 TTI。
  3. 如果没有找到 Long Task,以起始点时间作为 TTI。
  4. 如果 2、3 步骤得到的 TTI < DOMContentLoadedEventEnd,以 DOMContentLoadedEventEnd 作为 TTI

如何计算 TTI 指标–应用性能监控全链路版-火山引擎

TBT - Total Blocking Time

总阻塞时间,衡量从 FCP 到 TTI 之间主线程被阻塞的总时间
大于 50ms 的任务被称为长任务。
TBT 计算的就是每个任务时长 - 50ms 后,剩余的总和

TTFT - Time to First Byte

首次字节时间,衡量从浏览器发送请求到接收到服务器响应的第一个字节所需的时间
计算方式:

1
2
3
4
5
6
7
8
9
// 获取所有资源加载的详细信息
const resourceEntries = performance.getEntriesByType('resource');
for (const entry of resourceEntries) {
if (entry.initiatorType === 'fetch' && entry.name === 页面URL) { // 或其他匹配条件
const ttfb = entry.responseStart - entry.fetchStart;
console.log(`TTFB for ${entry.name}: ${ttfb} ms`);
break; // 如果只需要主文档的TTFB
}
}

可以使用:web-vitals 库获取具体 TTFT 值

参考资料

最全的前端性能定位总结 - 掘金
如何根据页面的 timing 指标计算出各阶段值–应用性能监控全链路版-火山引擎
Google Web Vitals


1-6、JS 性能优化
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/1-6、JS 性能优化/
作者
黄智强
发布于
2024年1月13日
许可协议