代码视界

Hanpeng Chen的个人博客

前端性能指标:白屏和首屏时间的计算

本文于 1172 天之前发表,文中内容可能已经过时。

前言

页面性能优化是前端开发中一个重要的环节,而评判前端性能的优劣有两个比较经常听说的指标:白屏时间和首屏时间。

接下来我们一起来看看什么是白屏时间和首屏时间,如何去计算。

什么是白屏和首屏时间

白屏时间(FP)

白屏时间(First Paint):是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。

  • 白屏时间 = 页面开始展示的时间点 - 开始请求的时间点

首屏时间(FCP)

首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。

  • 首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点

欢迎关注我的微信公众号:前端极客技术(FrontGeek)

常规计算方式

白屏时间

白屏时间是从用户开始请求页面时开始计算到开始显示内容结束,中间过程包括DNS查询、建立TCP链接、发送首个HTTP请求、返回HTML文档、HTML文档head解析完毕。

因此影响白屏时间的因素:网络、服务端性能、前端页面结构设计。

通常认为浏览器开始渲染<body>或者解析完<head>的时间是白屏结束的时间点。所以我们可以在html文档的head中所有的静态资源以及内嵌脚本/样式之前记录一个时间点,在head最底部记录另一个时间点,两者的差值作为白屏时间

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>白屏时间计算-常规方法</title>
<script>
window.pageStartTime = Date.now()
</script>
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/ionicons/2.0.1/css/ionicons.min.css">
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/asset/fw-icon/1.0.9/iconfont.css">
<script>
window.firstPaint = Date.now()
console.log(`白屏时间:${window.firstPaint - window.pageStartTime}`)
</script>
</head>
<body>
<div>这是常规计算白屏时间的示例页面</div>
</body>
</html>

白屏时间 = window.firstPaint - window.pageStartTime

这个方法有个缺点:无法获取解析HTML文档之前的时间信息。

首屏时间

首屏时间要知道两个时间点:开始请求时间点和首屏内容渲染结束时间点。开始请求时间点和白屏时间一样,下面就是如何拿到首屏内容渲染结束时间点。

首屏结束时间应该是页面的第一屏绘制完,但是目前没有一个明确的API可以来直接得到这个时间点,所以我们只能智取,比如我们要知道第一屏内容底部在HTML文档的什么位置,那么这个第一屏内容底部,也称之为首屏线

现在的问题是:首屏线在哪里?

实际情况分很多种,不同的场景有不同的计算方式。

标记首屏标签模块

这种方式比较简单,通过在HTML文档中,在首屏线的位置添加脚本去获取这个位置的时间。

但是在哪里添加我们只能大约估摸一个位置,以手机屏幕为例,不同型号手机屏幕大小不一样,我们取的位置可能在首屏线上面,也可以在下面,只能得到一个大约的值。

代码实现

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首屏时间计算-标记首屏标签模块</title>
<script>
window.pageStartTime = Date.now()
</script>
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/ionicons/2.0.1/css/ionicons.min.css">
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/asset/fw-icon/1.0.9/iconfont.css">
</head>

<body>
<div>首屏时间计算-标记首屏标签模块</div>
<div class="module-1"></div>

<div class="module-2"></div>

<script type="text/javascript">
window.firstScreen = Date.now();
console.log(`首屏时间:${window.firstScreen - window.pageStartTime}`)
</script>
<div class="module-3"></div>

<div class="module-4"></div>
</body>

</html>

首屏时间 = window.firstScreen - window.pageStartTime

这种方法的适用场景:

  • 首屏内不需要拉取数据,否则可能拿到首屏线获取时间的时候,首屏还是空白
  • 不需要考虑图片加载,只考虑首屏主要模块

在业务中,较少使用这种算法,大多数页面需要使用接口,所以这种方法就太不常用

但是如果你的页面是静态页面,或者异步数据不影响整体的首屏体验,那么就可以使用这种办法

统计首屏最慢图片加载时间

我们知道在一个页面中,图片资源往往是比较后加载完的,因此可以统计首屏加载最慢的图片是否加载完成,加载完了,记录结束时间。

如何知道首屏内哪张图片加载最慢?

我们可以拿到首屏内所有的图片,遍历它们,逐个监听图片标签的onload事件,并收集到它们的加载时间,最后比较得到加载时间的最大值。

代码实现:

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首屏时间计算-统计首屏最慢图片加载时间</title>
<script>
window.pageStartTime = Date.now()
</script>
</head>

<body>
<img src="https://image.chenhanpeng.com/static/blog-images/blogImages/2021/spring/20210107155629.png" alt="img" onload="load()">
<img src="https://image.chenhanpeng.com/static/blog-images/blogImages/2020/autumn/article-gzh-qrcode.png" alt="img" onload="load()">
<script>
function load() {
window.firstScreen = Date.now()
}
window.onload = function () {
// 首屏时间
console.log(window.firstScreen - window.pageStartTime)
}
</script>
</body>
</html>

首屏时间 = window.firstScreen - window.pageStartTime

适用场景:

  • 首屏元素数量固定的页面,比如移动端首屏不论屏幕大小都展示相同数量的内容。

但是对于首屏元素不固定的页面,这种方案并不适用,最典型的就是PC端页面,不同屏幕尺寸下展示的首屏内容不同。上述方案便不适用于此场景。

自定义模块计算法

这种算法和标记首屏的方法相似,同样忽略了首屏内图片加载的情况,这个方法主要考虑的是异步数据。

在首屏标签标记法中,是无法计算到异步数据带来的首屏空白的,所以它的适配场景十分有限

自定义模块,就是根据首屏内接口计算比较得出最迟的时间

代码实现:

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首屏时间计算-自定义模块计算法</title>
<script>
window.pageStartTime = Date.now()
</script>
</head>

<body>
<div class="module-1"></div>

<div class="module-2"></div>
<script type="text/javascript">
setTimeout(() => {
// 假设这里异步加载首屏要显示的文章列表数据
window.firstScreen = Date.now();
console.log(window.firstScreen - window.pageStartTime)
}, 500)
</script>
<div class="module-3"></div>
</body>

</html>

window.performance

上面我们学习了几种常规的计算首屏和白屏时间的方法,其实现在w3c提供了一个用来测量网页和web应用程序的API:window.performance,通过这个API,我们可以更方便获取到相应的时间节点。

这个API在IE9以上的浏览器都支持。

window.performance是一个浏览器中用于记录页面加载和解析过程中关键时间点的对象,放置在global环境下,通过JavaScript可以访问到它。

通过以下代码可以探测和兼容performance:

1
2
3
4
5
6
var performance = window.performance || 
window.msPerformance ||
window.webkitPerformance;
if (performance) {
// 你的代码
}

performance属性

我们一起来看下performance都有哪些属性:

  • memory:显示此刻内存占用情况,是一个动态值
    • usedJSHeapSize:JS对象占用的内存数
    • jsHeapSizeLimit:可使用的内存
    • totalJSHeapSize:内存大小限制

正常usedJSHeapSize不大于totalJSHeapSize,如果大于,说明可能出现了内存泄漏。

  • navigation:显示页面的来源信息

    • redirectCount:表示如果有重定向的话,页面通过几次重定向跳转而来,默认为0
    • type:表示页面打开的方式。0-正常进入;1-通过window.reload()刷新的页面;2-通过浏览器的前进后退按钮进入的页面;255-非以上方式进入的页面。
  • onresourcetimingbufferfull:在resourcetimingbufferfull事件触发时会被调用的一个event handler。它的值是一个手动设置的回调函数,这个回调函数会在浏览器的资源时间性能缓冲区满时执行。

  • timeOrigin:一系列时间点的基准点,精确到万分之一毫秒。

  • timing:一系列关键时间点,包含网络、解析等一系列的时间数据。

timing中的时间点

下面我们来看下timing中的各个时间点:

  • navigationStart:同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同

  • unloadEventStart: 上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。

  • unloadEventEnd: 和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。

  • redirectStart: 第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0

  • redirectEnd: 最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0

  • fetchStart: 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。

  • domainLookupStart: DNS 域名查询开始的UNIX时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。

  • domainLookupEnd: DNS 域名查询完成的时间。如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等

  • connectStart: HTTP(TCP) 域名查询结束的时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。

  • connectEnd: HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。

  • secureConnectionStart: HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。

  • requestStart: 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。

  • responseStart: 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。

  • responseEnd: 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时。(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。

  • domLoading: 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。

  • domInteractive: 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。

  • domContentLoadedEventStart: 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。

  • domContentLoadedEventEnd: 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。

  • domComplete: 当前文档解析完成,即Document.readyState 变为 ‘complete’且相对应的readystatechange 被触发时的时间戳

  • loadEventStart: load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。

  • loadEventEnd: 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0

通过上面这些时间点,我们看能计算到哪些时间:

  • 重定向耗时:redirectEnd - redirectStart
  • DNS查询耗时:domainLookupEnd - domainLookupStart
  • TCP链接耗时:connectEnd - connectStart
  • HTTP请求耗时:responseEnd - responseStart
  • 解析dom树耗时:domComplete - domInteractive
  • 白屏时间:responseStart - navigationStart
  • DOM ready时间:domContentLoadedEventEnd - navigationStart
  • onload时间:loadEventEnd - navigationStart

资源性能API

performance.timing记录的是用于分析页面整体性能指标。如果要获取个别资源(例如JS、图片)的性能指标,就需要使用Resource Timing API。

performance.getEntries()方法,包含了所有静态资源的数组列表;每一项是一个请求的相关参数有name,type,时间等等。

除了performance.getEntries之外,performance还包含一系列有用的方法,比如:

  • performance.now()
  • Performance.getEntriesByName()
  • ….

上面这些方法我不具体介绍,大家可以自行查阅相关文档了解,这里我主要说一下我们可以利用getEntriesByName()这个方法来计算首屏时间:

首屏时间:performance.getEntriesByName(“first-contentful-paint”)[0].startTime - navigationStart

欢迎关注我的微信公众号:前端极客技术(FrontGeek)

我该计算首屏时间还是白屏时间?

在评估页面是否开始渲染方面,首屏时间会比白屏时间更精确,但是二者的结束时间往往很接近。所以要根据自己的业务场景去决定到底该用哪种计算方式。

对于交互性比较少的简单网页,由于加载比较快,所以二者区别不大,因此,可以根据喜好任选一种计算方式。

对于大型的复杂页面,你会发现由于需要处理更多复杂的元素,白屏时间和首屏时间相隔比较远,这时候,计算首屏时间会更有用。

白屏和首屏的优化

目前白屏常见的优化方案有:

  • SSR
  • 预渲染
  • 骨架屏

优化首屏加载时间的方法:

  • CDN分发(减少传输距离)
  • 后端在业务层的缓存
  • 静态文件缓存方案
  • 前端的资源动态加载
  • 减少请求的数量
  • 利用好HTTP压缩

白屏时间和首屏时间的优化方法不止上面这些,感兴趣的小伙伴可以自行查找相关内容。

欢迎关注微信公众号: 『前端极客技术』『代码视界』
支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励