前端性能优化方法论(一)

前端性能优化方法论笔记

Google 研究所给出的数据:

  • 如果一个页面加载时间超过 3s,将损失 53% 的用户
  • 一个有10条数据0.4秒能加载完的页面,变成30条数据0.9秒加载完之后,流量和广告收入下降90%
  • Google Map 首页文件大小从100KB减小到70-80KB后,流量在第一周涨了10%,接下来的三周涨了25%

雅虎35条军规

1. 减少 HTTP 请求数

2. 使用精灵图

3. 字体图标 iconfont

4. svg图标

5. 合并 JS 和 CSS 资源

6. 图片改为内联图片(base64)

7. 使用CDN

8. 服务器端添加 Expires 或 Cache-Control 请求头(主要是为了做浏览器缓存)

9. 服务器端对某些内容做 Gzip 压缩

引发性能问题可能的原因:

  • 加载、网络性能问题
  • 渲染性能问题
  • 内存性能问题
  • 算法性能问题
  • 设计性能问题

如何科学的衡量当前性能状态、科学的进行分析?

1. 度量:科学的去衡量当前的性能状况

2. 分析:分析当前所存在的性能问题,做对应的优化

3. 实验:验证性能优化后的效果

度量

快与慢本身其实是一个比较主观的评价。

所以在性能优化的第一阶段,不是着急去做任何优化,而是应该去度量。

统计指标

常见的统计指标,有如下三种:

1. 均值

2. 分位数

3. 秒开率

1. 均值

多个用户访问首屏的时间平均值。

均值可能存在的问题:

1. 极端值

用户耗时
用户A100ms
用户B300ms
用户C900ms
用户D1200ms

2. 可解释性

有1万名用户,舍弃极限值后计算出来的首屏均值为 1s,但是这并不能说明所有用户都处于一个很好的性能状况。

均值肯定是有指导意义的,但是不能仅仅作为判断的唯一标准。

2. 分位数

常常和均值一起使用的指标,可以很好的解决均值指标所存在的极限值和解释性的问题。

所谓分位数,分位数将数据集分成相等的部分,每部分包含相同数量的数据点。例如,四分位数将数据分成四等分,百分位数将数据分成一百等分。

分位数里面有一个特殊情况是中位数,中位数是一个特定的分位数,即第50**百分位数**,用于描述数据的中心位置。中位数的计算方法:

1. 将数据按照升序排列

2. 确定数据集的个数(n)

3. 按照n的奇偶性计算中位数

– 如果 n 是奇数,中位数为 (n+1)/2 个数据点

– 如果 n 是偶数,中位数为 n/2 和 n/2+1 个数据点的平均值

示例1:奇数个数据点

数据集:3, 1, 4, 1, 5, 9, 2

排序后:1, 1, 2, 3, 4, 5, 9

数据集个数:n = 7(奇数)

中位数:3
数据集:7, 1, 3, 8

排序后:1, 3, 7, 8

数据集个数:n = 4(偶数)

中位数:5

示例3: 含有重复值

数据集:5, 5, 5, 1, 1, 1, 7, 7, 7

排序后:1, 1, 1, 5, 5, 5, 7, 7, 7

数据集个数:n = 9(奇数)

中位数:5

示例4: 含有极端值的数据

数据集:1, 2, 3, 4, 1000

排序后:1, 2, 3, 4, 1000

数据集个数:n = 5(奇数)

中位数:3
中位数优点:
  • 对极端值不敏感
  • 适用于偏态分布
  • 易于理解和计算

3. 秒开率

分位数侧重于性能差的用户端的性能情况,秒开率关注的是有多少用户端可以达到非常高的性能水平。秒开率一般统计的是1s 内打开页面的用户占比。


Google 推出的 Web Vitals,这是一个标准化指标集合。Web Vitals 涵盖了用户体验的几个核心方面,包括加载性能、交互性和视觉稳定性。

  • 核心指标:LCP、FID、CLS
  • 扩展指标:FCP、TTI、TBT
  1. 首屏渲染相关指标

2. 流畅度相关指标

首屏渲染相关指标

FCP

英语全称为“First Contentful Paint”,翻译成中文是“第一次内容绘制”。也就是指用户从打开应用到肉眼可以看到界面上第一次呈现出内容的时间。这里的内容可以上任何界面呈现,文本、图片、视频等等。

LCP

英语全称为“Largest Contentful Paint”,翻译成中文是“最大块内容首次绘制”,也就是从你的应用打开,到全部内容加载完成之前,这一段时间里面,界面上最大的一块内容在什么时间点上被完整渲染出来。LCP是一个**动态的评价过程**,因为当界面没有加载完之前,你也说不准到底哪一块才是界面上最大的一块内容。

CLS

英语全称是“Cumulative Layout Shift”,中文意思“累计布局偏移”,它是一个比值。在应用界面加载过程中,可见元素在前后两帧之间存在布局上的偏移,那么认为这个元素是不稳定的,比如用户准备用鼠标去点这个元素,结果点击点时候,这个元素被另外一个元素顶开了,导致点错了元素。CLS就是收集一段时间内这些偏移的数据。它的计算公式是

JavaScript
impact fraction * distance fraction

在上面的示例中,左侧(也就是前一帧)图片内的元素占据了视口的一半区域,右侧(也就是后一帧)图片内的元素向下移动了视口高度的 25%。红色虚线框表示的就是这两帧中元素可见区域的**并集**,也就是 75%, 所以 impact fraction 就是 0.75。最大的视口尺寸是高度,不稳定元素移动了视口高度的 25%,因此 distance fraction 就是 0.25。最后可以得到布局移位就是 `0.75 * 0.25 = 0.1875`。

流畅度相关指标

FID

英文全称为“First Input Delay”,翻译成中文是“第一次输入延迟”,即用户第一次在界面上进行操作(例如点击某个链接)到程序对这个操作做出回应的延迟。这是因为经常我们的应用需要通过网络请求脚本或数据来进行渲染,而在请求的过程中,我们的程序或者浏览器本身,无法响应用户的操作,这个无法响应的过程,就是FID。

FID 只关注来自离散操作的输入事件,如单击事件、按键事件, 像滚动和缩放这样的连续交互动作是不考虑的。

TTI

英语全称为“Time to Interactive”,中文是“达到可稳定交互的时间”,假如你不做任何输入,让应用自己打开,直到整个应用没有发生任何动作(静默状态)持续5秒钟,那么在这5秒钟之前的那一次长任务的结束时间,就是TTI.

通过TTI和FID的结合,可以评判出你的应用在什么的交互体验如何。

FPS

FPS 的英语全称为“Frame Per Second”,中文叫做“每秒帧数”。它是一个速度指标“帧率”,即在每秒内刷新了多少帧。

浏览器的绘制受多方面的影响,理论上在没有任何影响下,浏览器的帧率 60Hz,即 16.66ms 左右一帧,但是由于浏览器执行时存在一些资源调度逻辑,需要等某些执行任务完成之后,才能进入下一帧刷新,这也就导致了丢帧现象,我们都知道人肉眼可辨为 0.1s 每帧,超过这个时长就会感觉卡顿,所以通过 FPS 指标来衡量应用的性能也是非常重要的。

前面所有的指标,都是基于“第一次原则”建立,只有 FPS 是基于“持续性原则”。

TBT

英语全称为“Total Blocking Time”,指的是“总阻塞时长”。长任务执行会阻塞界面的渲染,有的卡顿明显,有的不明显,在FCP到TTI之间,这些长任务的阻塞时长的总和,就是TBT。

获取性能指标

  • 开发阶段:主要依赖浏览器提供的工具进行性能问题的诊断和排查
  • 生产阶段:监控系统

在前端收集性能数据时,常常会用到如下接口:

  • perfermance
  • PerformanceObserver
  • web-vitals

PerformanceAPI

Performance API 是一套由**浏览器提供**的 Web API. 注意这个并不是 ES 语言规范里面的,而是由 W3C 制定的 Web 性能标准的一部分,专门用于测量和监控网页和 Web 应用程序的性能,提供了**一套接口**以帮助开发者了解和优化页面的加载时间和运行效率。

Performance API 是一套 API,常见的成员有:

  • performance对象
  • PerformanceObserver类

在早期为了测量一个任务的耗时情况,我们最容易想到的是 Date.now 方法

JavaScript
const startTime = Date.now();
const taskCostTime = Date.now() - startTime;

这种方式存在两个问题:

  1. 在部分场景下精度不够,只能精确到毫秒

2. Date API 本身就不是用来测量性能的,而是处理时间的

performance 对象提供了 performance.now( ) 方法,由于这套 API 是专门用于性能相关测试的,所以精度也很高,能精确到**微秒**

实际上这个 now 方法是属于 performance 原型对象上面的方法,整个 performace 有这么一些属性:

  • eventCounts:提供了对页面上特定类型事件的统计信息。用于监控和分析页面上发生的事件的数量,例如点击、滚动等,有助于了解页面的交互频率和事件处理性能。
  • memory:提供了关于 JS 堆内存使用情况的信息。包含以下属性:
  • jsHeapSizeLimit:JS 堆的内存限制。
  • totalJSHeapSize:已分配的 JS 堆内存总量。
  • usedJSHeapSize:当前使用的 JS 堆内存量。
  • navigation:navigation 属性提供了与页面导航相关的信息。包含导航类型(例如,首次加载、刷新、后退等)和一些基本的导航计时数据(已被 PerformanceNavigationTiming 取代)。
  • onresourcetimingbufferfull:这是一个事件处理程序属性,用于处理 resourcetimingbufferfull 事件。当资源计时缓冲区已满时触发的事件处理程序,可以用于处理和清理缓冲区数据。
  • timeOrigin:提供了用于当前页面的性能计时的起始时间。这是一个高精度时间戳,从该时间点开始计算 performance.now() 的值。这个时间戳通常是导航开始的时间。
  • timing:该属性提供了有关**页面加载各个阶段的详细时间数据**(已被 PerformanceNavigationTiming 取代)。

PerformanceNavigationTiming 是 Performance API 的一个接口,它提供了更详细和准确的页面导航和加载性能数据。

PerformanceNavigationTiming 的优势如下:

  • 更精细的时间戳
  • 统一的接口
  • 更好地支持现代浏览器

常见属性如下:

  • domComplete:文档和所有资源完全加载和解析完成的时间。
  • domContentLoadedEventEnd:DOMContentLoaded 事件处理完成的时间。
  • domContentLoadedEventStart:DOMContentLoaded 事件开始的时间。
  • domInteractive:文档解析完成且能够操作的时间。
  • loadEventEnd:load 事件处理完成的时间。
  • loadEventStart:load 事件开始的时间。
  • navigationStart:导航开始的时间。
  • redirectCount:重定向的次数。
  • requestStart:浏览器向服务器发出请求的时间。
  • responseEnd:浏览器接收到完整响应数据的时间。
  • responseStart:浏览器开始接收响应数据的时间。

– f etchStart:浏览器准备好使用 HTTP 请求抓取资源的时间。

JavaScript
const [navigationEntry] = performance.getEntriesByType('navigation');

console.log(`导航开始时间: ${navigationEntry.navigationStart}`);

console.log(`重定向次数: ${navigationEntry.redirectCount}`);

console.log(`请求开始时间: ${navigationEntry.requestStart}`);

console.log(`响应开始时间: ${navigationEntry.responseStart}`);

console.log(`响应结束时间: ${navigationEntry.responseEnd}`);

console.log(`DOM 解析完成时间: ${navigationEntry.domInteractive}`);

console.log(`DOMContentLoaded 事件开始时间: ${navigationEntry.domContentLoadedEventStart}`);

console.log(`DOMContentLoaded 事件结束时间: ${navigationEntry.domContentLoadedEventEnd}`);

console.log(`load 事件开始时间: ${navigationEntry.loadEventStart}`);

console.log(`load 事件结束时间: ${navigationEntry.loadEventEnd}`);

console.log(`文档加载完成时间: ${navigationEntry.domComplete}`);

PerformanceObserver

PerformanceObserver 提供了一种异步、非阻塞的方式来监控和收集各种性能指标。通过使用 PerformanceObserver,可以实时地收集性能数据,如页面加载时间、资源加载时间、长任务等,然后根据需要进行处理和分析。

基本使用如下:

  1. 创建 PerformanceObserver 实例:首先需要创建一个 PerformanceObserver 实例,并传递一个回调函数。当所监控的性能条目被记录时,这个回调函数会被调用。
JavaScript
// 创建 PerformanceObserver 实例,并定义回调函数

// list 是一个 PerformanceObserverEntryList 对象,包含了被记录的性能条目。

// 这些性能条目是以 PerformanceEntry 对象的形式存在的。

// PerformanceObserverEntryList 提供了以下主要方法:

// 1. getEntries():返回所有被观察到的性能条目。

// 2. getEntriesByType(type):根据指定的条目类型返回匹配的性能条目。

// 3. getEntriesByName(name, type):根据指定的名称和可选的条目类型返回匹配的性能条目。

const observer = new PerformanceObserver((list) => {

// 获取所有性能条目

const entries = list.getEntries();

// 遍历并输出每个性能条目

entries.forEach((entry) => {

console.log(entry);

});

});

// 开始观察特定类型的性能条目,比如 'resource' 和 'paint'

observer.observe({ entryTypes: ['resource', 'paint'] });

2. 选择要观察的条目类型: 接下来,调用 observe 方法并指定要观察的条目类型。可以指定一个或多个条目类型。

observer.observe({ type: 'paint', buffered: true });
  • type:指定要观察的性能条目类型(例如 ‘paint’, ‘resource’, ‘longtask’ 等)
  • paint:页面绘制相关的条目,包括 FCP(First Contentful Paint)和 FP(First Paint)。
  • resource:资源加载相关的条目,表示外部资源(如图片、脚本)的加载时间。
  • longtask:长任务条目,表示主线程上执行时间超过 50 毫秒的任务。
  • largest-contentful-paint:表示页面上最大内容元素的绘制时间。
  • layout-shift:布局偏移条目,表示页面上发生的布局变化。
  • first-input:首次输入延迟条目,表示用户首次与页面交互(如点击、按键)的延迟时间。
  • buffered:可选参数,表示是否观察缓冲区中已经记录的条目。

获取 FCP

JavaScript
new PerformanceObserver((entryList) => {

for (const entry of entryList.getEntriesByName('first-contentful-paint')) {

console.log(`First Contentful Paint: ${entry.startTime}`);

}

}).observe({ type: 'paint', buffered: true });

获取 LCP

JavaScript
new PerformanceObserver((entryList) => {

for (const entry of entryList.getEntries()) {

console.log(`Largest Contentful Paint: ${entry.startTime}`);

}

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

获取 CLS

JavaScript
let clsValue = 0;

new PerformanceObserver((entryList) => {

for (const entry of entryList.getEntries()) {

if (!entry.hadRecentInput) {

clsValue += entry.value;

}

}

console.log(`Cumulative Layout Shift: ${clsValue}`);

}).observe({ type: 'layout-shift', buffered: true });

获取 FID

JavaScript
new PerformanceObserver((entryList) => {

for (const entry of entryList.getEntries()) {

console.log(`First Input Delay: ${entry.processingStart - entry.startTime}`);

}

}).observe({ type: 'first-input', buffered: true });

获取 FPS

JavaScript
let frameCount = 0;

let lastTime = performance.now();

let fps = 0;

function calculateFPS() {

const now = performance.now();

frameCount++;

const duration = now - lastTime;

if (duration >= 1000) {

fps = (frameCount / duration) * 1000;

frameCount = 0;

lastTime = now;

console.log(`FPS: ${fps}`);

}

requestAnimationFrame(calculateFPS);

}

requestAnimationFrame(calculateFPS);

Google 提供了一个名为 web-vitals 的 JavaScript 库,方便开发者在网页中收集这些性能指标。

安装库:

Bash
npm install web-vitals
JavaScript
import { onLCP, onINP, onCLS } from "web-vitals";

onCLS(console.log);

onINP(console.log);

onLCP(console.log);

使用库来测量核心 Web Vitals:

Performance面版

浏览器控制台中的 Performance 面版,其实就是 Performance API 数据的 GUI 呈现,浏览器通过 Performance API 收集性能数据,并在 Performance 面板中以图形化的方式展示这些数据,以帮助开发者分析和优化网页性能。

Performance 面板基于时间线 timeline 对应用运行过程进行了切面汇总,创造性的绘制了一张火焰图,通过这张火焰图,可以直接定位到哪一段代码运行了特别长时间。

另外一个常用的面版是 Lighthouse,这是一个基于谷歌评价指标的全站评测工具,其中不仅包含了性能指标,还包含了其他指标,我们可以只关注其中的性能指标,通过lighthouse,我们可以获得对自己网站的一份评测报告。

Chrome60 内置了 Lighthouse 到 devtool 面板中。

监控系统

在生产阶段,往往需要一些成熟的性能监控平台来辅助我们等到性能指标数据。这种性能监控平台很多:

1. sentry https://sentry.io/welcome/

2. 灯塔

3. arms https://www.aliyun.com/product/arms

4. 神策

分析

整个分析可以按照如下的方法:

1. 确定目标

2. 收集数据

3. 清洗数据

4. 统计值分析

5. 时序分析

6. 维度分析

7. 确认端分析

1. 确定目标

首先第一步需要确定一个分析目标。

  • 找到对 LCP 影响最大的因素
  • 找到某个页面 FCP 突然增加的原因
  • 分析某个 API 请求为什么需要 2000ms 才完成

2. 收集数据

目标确定了之后,接下来就是围绕这个目标,收集相关的数据。

3. 清洗数据

所谓清洗,指的就是去除一些无效的值,例如空值、零值、异常值。

4. 统计值分析

统计值分析也称之为描述性分析,简单来讲,就是将各种能够得到的数据指标一一列举出来,然后观察其特征。

举个例子:

比如我们现在在分析一个页面 API 加载时候均值为什么特别大时,可以获取其 API 加载的开始时间、API 请求本身的耗时。把这些指标的统计值(如均值)都放在表格中,通过直接观察就能得出一些结论。

时间段耗时
页面加载到发起请求时间 800ms
请求本身的耗时300ms
页面加载到请求结束的时间1100ms

5. 时序分析

时序分析也是相对直观的一种分析方式。所谓时序分析,就是指观察某些指标随着时间的变化是否存在一些变化。通过对随着时间的变化性能指标发生的变化走势,能够捕捉到由变更(如新发版)造成的影响。

举个例子,同样是上面的问题,但是把性能按照天的维度来做聚合,就会得到一组数据:

页面加载到发起请求的时间请求本身的耗时页面加载到请求结束的时间
1-1 200ms300ms500ms
1-2210ms302ms515ms
1-3800ms290ms11-2ms
1-4810ms300ms1100ms
1-5790ms302ms1090ms

6. 维度分析

在性能分析中,除了**时间**这个特殊的维度,还有一些其他常见的数据聚合度,比较典型的有:

  • 地域
  • 优化手段
  • 浏览器
  • 操作系统:特别是在移动端
时间段 iOS端耗时Android端耗时
页面加载到发起请求的时间300900
请求本身的耗时200220
页面加载到请求结束的时间5001120

7. 确认端分析

该指标的全称是“Time To First Byte”,指的是客户端从发起请求到接收到服务器响应的第一个字节的时间,这是反应应用性能的一个重要指标。与网页的下载时间相比,TTFB 不受传输容量、网络带宽的影响,一般来说能够更好的反应服务器端的性能。

更加准确的来讲,TTFB 的时间计算是以下几个时间的综合:

1. DNS解析时间:将域名解析为IP地址的时间。

2. TCP连接时间:客户端与服务器之间建立TCP连接所花费的时间。

3. SSL握手时间(如果使用了HTTPS):建立安全连接的时间。

4. 服务器处理时间:服务器处理请求并生成响应的时间。

5. 数据传输时间:服务器将第一个字节的数据发送到客户端的时间。

3. 实验

性能问题找到之后,接下来就是使用具体的优化手段来进行优化。不同的性能问题,会用到不同的优化手段。例如:

  • 页面加载速度慢
  • 减少HTTP请求
  • 启用压缩
  • 使用内容分发网络
  • 延迟加载
  • 减少重定向
  • 页面渲染慢:
  • 优化 CSS 和 JS
  • 使用服务端渲染
  • 启用浏览器缓存
  • …..

使用A/B测试对所选择的优化手段进行效果验证。A/B 测试(也称为分离测试或对比测试)是一种实验方法,将用户随机分成两组,分别展示两个不同的版本(A 和 B),以比较两个版本的效果。

具体步骤:

1. 设定目标

2. 选择变量

3. 创建版本:一般A是现有版本,B对照版本

4. 随机分组

5. 数据收集

6. 分析结果

对用户进行分组又被称之为分桶(Bucket Testing),用户被分成不同的组(桶)进行测试。每个桶内的用户会接触不同的实验条件,之后收集每个桶内用户的行为数据,比较不同桶之间的效果差异,确定最佳版本。

分桶有一定的策略:

  • 完全随机分桶
  • 分层抽样
  • 时间分桶

来举一个实际的例子,比如 HTML 中的图片,除了使用 URL 的方式以外,还可以使用 base64 编码

“`html

HTML
<!-- 普通的URL引用 -->

<img src="https://img.examplease/a.png"/>

<!-- base64后内联 -->

<img src="data:image/gif;base64,R01GOD1hEAAQWSKJDhdjj12kdfq9FDN...."/>

使用 base64 的方式可以减少一个图片请求的开支,只需要一个请求就能把图片和页面一起拉下来,并且节约了图片域名解析和建立连接的时间。然而这种做法也有一些缺点:

  • Base64 会导致图片体积增加 1/3
  • 客户端解析 base64 需要花费时间
  • HTML 中内联的图片多次请求之间是无法复用缓存的

可以做一个 A/B 测试来验证该优化手段是否有效

1. 确定测试目标

确定在页面中使用 Base64 编码内联图片和通过 URL 加载图片的两种方式中,哪种方式对页面加载性能和用户体验的影响更好。

2. 确定测试假设

  • 假设A(URL方式):使用 URL 加载图片会比 Base64 编码方式加载页面更快,因为避免了 Base64 编码增加的体积以及客户端解析 Base64 编码的时间。
  • 假设B(Base64 方式):使用 Base64 编码内联图片会比 URL 方式加载页面更快,因为减少了 HTTP 请求和域名解析时间。

3. 用户分桶

  • A组:使用 URL 方式加载图片。
  • B组:使用 Base64 编码内联图片。

4. 确定测试指标

  • 页面加载时间
  • 首屏渲染时间
  • 完全加载时间
  • 用户互动时间

5. 开始 A/B 测试

1. 创建两个版本的页面

HTML
<!-- 版本 A: 使用 URL 方式 -->

<!DOCTYPE html>

<html>

<head>

<title>Image Load Test - URL</title>

</head>

<body>

<img src="https://img.example.com/a.png" alt="Image using URL" />

<!-- 其他页面内容 -->

</body>

</html>
HTML
<!-- 版本 B: 使用 Base64 编码内联 -->

<!DOCTYPE html>

<html>

<head>

<title>Image Load Test - Base64</title>

</head>

<body>

<img src="data:image/gif;base64,R0lGODlhPQBEAPeoAJosM...." alt="Image using Base64" />

<!-- 其他页面内容 -->

</body>

</html>

2. 配置 A/B 测试工具(Google Optimize、Optimizely ),随机选取一定数量的用户流量,将用户随机分配到版本 A 或版本 B。

3. 在页面上添加性能监控代码,记录每个用户的页面加载时间、首屏渲染时间、完全加载时间和用户互动时间。

JavaScript
// 示例:使用 Performance API 监控页面加载时间

window.addEventListener('load', () => {

const timing = performance.timing;

const loadTime = timing.loadEventEnd - timing.navigationStart;

const fcp = performance.getEntriesByName('first-contentful-paint')[0].startTime;

const tti = performance.getEntriesByName('interactive')[0].startTime;

// 发送数据到服务器或 A/B 测试工具

sendPerformanceData({

loadTime: loadTime,

fcp: fcp,

tti: tti

});

});

function sendPerformanceData(data) {

// 示例:发送数据到服务器

fetch('/logPerformanceData', {

method: 'POST',

headers: {

'Content-Type': 'application/json'

},

body: JSON.stringify(data)

});

}

6. 数据分析

  • 收集一定时间内的性能数据
  • 分析两个版本的性能指标
  • 使用统计工具确定差异

通过设计和实施这个 A/B 测试,可以客观地评估使用 URL 和 Base64 编码两种方式加载图片的性能表现,从而做出数据驱动的决策,以数据来支持某一种方式是否能带来性能收益。


-EOF-

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注