Web性能知识(二)

Author Avatar
GeniusFunny 2月 14, 2020
  • 在其它设备中阅读本文章

Web性能取决于:资源获取【网络I/O】 + 页面布局与渲染 【浏览器渲染原理】+ JavaScript执行【事件循环】

这是一篇讲解前端性能量化体系的文章,参考后续补上;内容都是自己的笔记

前端性能量化体系

加载时间是我们用于评价前端性能好坏的一个参考数据,但是加载时间与用户使用的手机类型以及当前网络状况有关,除此之外,即使页面加载完成用户也可能无法操作,所以仅仅用一个加载时间是不能描述前端性能的好坏,我们需要一套完整的性能评价体系。

此前,我们有两个主要的事件来测量性能:

  • DomContentLoaded:页面加载时触发,但脚本刚刚开始执行

  • load:在页面完全加载后触发,此时用户已经可以使用页面和应用

但是,如果在网络环境较差或者脚本执行时间太长的时候,我们从DCL到load会经历一段很长很长的时间,并且load事件触发太晚,就无法分析出页面的性能瓶颈,所以我们需要建立一个从页面开始加载到加载完成的评价体系,记录用户整个感知过程。

渐进式网页指标

img

image-20191219094924938

首次绘制(FP): 标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点

首次内容绘制(FCP):标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 <canvas> 元素。

首次有效绘制(FMP)/主角元素计时:标记的是页面主要内容绘制的时间点,例如视频应用的视频组件、天气应用的天气信息、新闻应用中的新闻条目。

可交互时间(TTI):页面渲染完成,能可靠的响应用户的操作的时间点。(这个时间不仅取决于之前的加载时间,还取决于当前是否有长任务阻塞主线程)

浏览器页面加载流程

先谈谈浏览器的页面大致的加载流程:

  1. 浏览器输入url,浏览器发送请求到服务器,服务器发送HTML给浏览器。
  2. 浏览器下载HTML然后从上往下解析
  3. 如果在解析HTML过程中遇到CSS、JS外链,会执行下列操作
    1. Send Request:向这个外链对应的服务器发送请求
    2. Receive Response:接受响应,表示可以开始从网络接受数据
    3. Receive Data:开始接受数据
    4. Finish Loding:表示完成数据的下载
    5. Parse Stylesheet/Evaluate:解析CSS或执行JS
  4. 所有的CSS下载完成后进行parse,构建CSSOM
  5. DOM和CSSOM合成一个Render tree
  6. 根据Render tree的内容计算处各个节点在网页中的大小和位置(Layout)
  7. 根据Layout绘制内容在浏览器上(Paint)

首次绘制FP、首次内容绘制FCP

  • FP标记浏览器渲染任何在视觉上不同于导航前屏幕内容的时间点
    • 这个指标表示页面绘制的时间点,也就是用户第一次看到“白屏”的时间,标志“It is happening?”
    • 首次绘制包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻。
    • 什么时候触发FP事件呢?FP事件是在图层进行绘制的时候触发,而不是文本、图片、SVG等元素出现的时候(这个时候触发的是FCP事件)
  • FCP标记浏览器渲染来自DOM第一位内容的时间点
    • 当用户首次看见一些元素(文本、图片、SVG等),非”白屏“的时刻就是FCP,这个标志“it is running?”
    • FCP在文本、图片、SVG等绘制的时候就触发了,所以跟FP可以相差几毫秒~几秒的时间。
测量

通过PerformanceObserver或window.performance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 对于全局属性performance,可直接访问属性获取
const paint = performance.getEntriesByType('paint');
const FP = paint[0].startTime; // First Paint 时间
const FMP = paint[1].startTime; // First Contentful Paint 时间

//对于Performance Observer,需将其放置在所有样式表之前
const observer = new PerformanceObserver(list => {
for(const entry of list.getEntries()) {
const metricName = entry.name;
const time = Math.round(entry.startTime + entry.duration);
// 上报指数,例如使用Google Analytics
ga('send', 'event', {
eventCategory:'Performance Metrics',
eventAction: metricName,
eventValue: time,
nonInteraction: true,
});
}
});
observer.observe({entryTypes: ['paint']});
优化
  • 从文档中移除任何阻塞渲染的样式表和脚本
  • 将页面一开始需要的样式最小集内联到<head>或使用HTTP/2服务器推送

首次有效绘制FMP

FMP标记浏览器渲染页面主要内容的时间点,难以规范化

  • FMP就是页面主要内容呈现的时刻,目前没有标准化的FMP定义,所以很以通用的方式确定“有效”对于所有页面意味着什么。
  • 所以,我们怎么统计FMP呢?它的统计在页面发生了重大的布局变化之后;LayoutAnalyzer会统计所有的布局变化,当布局变化最大时,这个时刻就是FMP。
    • 如果主要内容渲染太慢,我们可以在样式、字体、脚本、图片上去思考,因为这些元素有很大可能阻塞了FMP。
测量

难以规范化测量页面中重要的内容,所以可以通过performance自定义化测量

1
2
3
4
5
6
7
8
9
10
11
12
13
// 开始渲染
function beginRender() {
performance.clearMarks('start render');
performance.mark('start render');
}
// 渲染完成,打印渲染完成的时间点
function finishRender() {
performance.clearMarks('finish render');
performance.mark('finish render');
performance.measure('render time', 'start render', 'finish render');
const renderTime = performance.getEntriesByName('render time');
console.log(renderTime.startTime + renderTime.duration);
}

可交互时间(Time To Interactive,TTI)

与 FMP 相同,很难规范化适用于所有网页的 TTI 指标定义,但我们也可以使用Google Analytics来跟踪应用TTI。

TTI发生在什么时候?FMP && DOMContentLoader事件触发 && 页面视觉加载85%

TTI又可以分为两个指标,TTFI(首次交互时间)、TTCI(首次持续交互时间)

TTFI: 在FMP发生后应该有3s的quiet window,这个时间足够说明页面对用户是可交互的,但是可能会有长任务在这个quiet window期间或之后开始执行,它们可以被忽略。

TTCI:从追终线的尾部开始看,页面加载后有5s的quiet并且没有长任务需要执行,得到了一段quiet window。quiet window之后到第一个长任务之前的时间就是TTCI。

影响上面某些指标的因素

FP/FCP:“白屏”,从文档的 <head> 中移除任何阻塞渲染的脚本或样式表,可以减少首次绘制和首次内容绘制前的等待时间。FCP:首次内容绘制耗时太长,直观上就是“白板时间太长”,影响因素就是网络连接存在性能问题、资源(例如index.html)太大传输耗时。

FMP/TTI:如果主要内容很久都没展示出来,那么很有可能是图片、样式、字体、JavaScript有较高的加载优先级,阻塞了FMP。确定页面上最关键的界面元素(主角元素)之后,您应确保初始脚本加载仅包含渲染这些元素并使其可交互所需的代码。

耗时长任务

耗时长的任务会影响用户体验(事件响应慢、掉帧)

统计耗时长任务:

1
2
3
4
5
6
7
8
9
10
11
12
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
ga('send', 'event', {
eventCategory:'Performance Metrics',
eventAction: 'longtask',
eventValue:Math.round(entry.startTime + entry.duration),
eventLabel:JSON.stringify(entry.attribution),
});
}
});

observer.observe({entryTypes: ['longtask']});
优化
  • 将耗时长任务切分为多个耗时段的异步任务(例如用setTimeout切割为n个宏任务)
  • 利用空闲阶段处理(requestIdleCallback)
输入响应慢

阻塞主线程的耗时较长任务可能会导致事件侦听器无法及时执行

统计输入响应慢的case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const subscribeBtn = document.querySelector('#subscribe');

subscribeBtn.addEventListener('click', (event) => {
// Event listener logic goes here...

const lag = performance.now() - event.timeStamp;
// 响应时间超过100ms,上报
if (lag > 100) {
ga('send', 'event', {
eventCategory:'Performance Metric'
eventAction: 'input-latency',
eventLabel: '#subscribe:click',
eventValue:Math.round(lag),
nonInteraction: true,
});
}
});

分析关键路径

优化关键渲染路径就是让刘篮球尽可能快地绘制网页,减少用户看到“白屏的时间”,从FP到FCP;所以整个时间就包括了获取资源(CSS、JS、HTML)以及浏览器处理对应的文件。

关键资源:可能阻止网页首次渲染的资源

关键路径长度:获取所有关键资源所需的往返次数或总时间

关键字节:实现网页首次渲染所需的总字节数,它是所有关键资源传送文件大小的总和。

HTML、Image

对于HTML,浏览器下载HTML文件,当HTML内容可用时,浏览器就会解析HTML,生成token,进而构建DOM树,DOM树构建完成同时意味着DOMContentLoaded事件触发;获取图片并不会影响DOMContentLoaded事件的触发,所以图片不会影响到首次绘制

关键资源(HTML),关键路径长度(最少1次),关键字节(5KB):

HTML、CSS

加载解析HTML,然后获取CSS文件,解析为CSSOM,生成渲染树,绘制页面。

关键资源(HTML、CSS),关键路径长度(最少2次),关键字节(9KB):

CSS、JavaScript、HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="timing.js"></script>
</body>
</html>

除了获取HTML、解析为DOM树,我们还会去加载CSS文件构建CSSOM,然后通过DOM和CSSOM构建渲染树,按道理来说,DCL也应该在DOM树生成后就触发,但是浏览器在下载解析CSS文件之前会阻止DCL事件,这是因为JS可能会去查询CSSOM。即使将脚本更改为内联,也无济于事。

所以,只要浏览器遇到script标记就会阻止,并等到CSSOM构建完毕。

关键资源(HTML、CSS、JS),关键路径(最少2次,获取css、js时为并行),关键字节(11KB)

async,异步脚本的用处
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="timing.js" async></script>
</body>
</html>

这种情况下,解析HTML之后不久就会触发DCL事件,浏览器知道不需要阻止JS,不必等到CSS文件加载解析后再执行JS。

关键资源(HTML、CSS),关键路径(最少2次),关键字节(9KB):

优化

上面提到,有三种因素(关键资源、关键路径、关键字节)影响着首次渲染,所以优化也应该从这三点入手。

  1. 减少关键资源的数量:删除、延迟、标记异步等
  2. 优化关键字节数以缩短下载时间(压缩)
  3. 优化其余关键资源的加载顺序(尽早下载所有的关键资产,缩短关键路径长度)

建立一套有效的性能测量机制

  • 统计FP、FCP、FMP、TTL数据
  • 上报耗时长任务、响应输入慢
  • 监控内存泄露情况

性能目标

响应:在100ms以内响应

在用户注意到滞后之前有100ms的时间可以响应用户输入。

动画:每10ms生成一帧

尽管每秒生成60帧,一帧16.6ms,但是每帧只有10ms来执行代码,其余的时间用于将新帧绘制到屏幕上。

空闲:最大程度增加空闲时间

利用空闲时间完成推迟的工作,换句话说,就是让主线程在UI线程和JS线程中切换。

加载:在1000ms以内呈现内容

在 1 秒钟内加载您的网站。

参考资料

  1. Performance metrics. What’s this all about?
  2. Paint Timing 1
  3. User-centric Performance Metrics
  4. User Timing and Custom Metrics
  5. 分析关键渲染路径性能
  6. 性能为何如此重要
  7. 使用RAIL模型评估性能