作者:Hanpeng_Chen
公众号:前端极客技术
文章首发个人博客:前端性能指标:白屏和首屏时间的计算|代码视界
前言
页面性能优化是前端开发中一个重要的环节,而评判前端性能的优劣有两个比较经常听说的指标:白屏时间和首屏时间。
接下来我们一起来看看什么是白屏时间和首屏时间,如何去计算。
什么是白屏和首屏时间
白屏时间(FP)
白屏时间(First Paint):是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。
- 白屏时间 = 页面开始展示的时间点 - 开始请求的时间点
首屏时间(FCP)
首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。
- 首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点
欢迎关注我的微信公众号:前端极客技术(FrontGeek)
常规计算方式
白屏时间
白屏时间是从用户开始请求页面时开始计算到开始显示内容结束,中间过程包括DNS查询、建立TCP链接、发送首个HTTP请求、返回HTML文档、HTML文档head解析完毕。
因此影响白屏时间的因素:网络、服务端性能、前端页面结构设计。
通常认为浏览器开始渲染<body>
或者解析完<head>
的时间是白屏结束的时间点。所以我们可以在html文档的head中所有的静态资源以及内嵌脚本/样式之前记录一个时间点,在head最底部记录另一个时间点,两者的差值作为白屏时间
代码实现:
<!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文档中,在首屏线的位置添加脚本去获取这个位置的时间。
但是在哪里添加我们只能大约估摸一个位置,以手机屏幕为例,不同型号手机屏幕大小不一样,我们取的位置可能在首屏线上面,也可以在下面,只能得到一个大约的值。
代码实现
<!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事件,并收集到它们的加载时间,最后比较得到加载时间的最大值。
代码实现:
<!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://gitee.com/HanpengChen/blog-images/raw/master/blogImages/2021/spring/20210107155629.png" alt="img" onload="load()">
<img src="https://gitee.com/HanpengChen/blog-images/raw/master/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端页面,不同屏幕尺寸下展示的首屏内容不同。上述方案便不适用于此场景。
自定义模块计算法
这种算法和标记首屏的方法相似,同样忽略了首屏内图片加载的情况,这个方法主要考虑的是异步数据。
在首屏标签标记法中,是无法计算到异步数据带来的首屏空白的,所以它的适配场景十分有限
自定义模块,就是根据首屏内接口计算比较得出最迟的时间
代码实现:
<!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:
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压缩
白屏时间和首屏时间的优化方法不止上面这些,感兴趣的小伙伴可以自行查找相关内容。
如果你觉得这篇内容对你有帮助的话:
1、点赞支持下吧,让更多的人也能看到这篇内容
2、关注公众号:前端极客技术,我们一起学习一起进步。