前端性能优化之首屏时间采集篇
所谓首屏,就是用户看到当前页面的第一屏,首屏时间就是第一屏被完全加载出来的时间点。
比如一个电商网站,首屏就包括导航栏、搜索框、商品头图等内容。那么,如何采集用户的首屏时间呢?
你可能会说,我直接用 Chrome DevTools 看一下就行了。
1、容易误导开发者的 Chrome DevTools
每次拿 Chrome DevTools 一看,好像自家网站的性能杠杠的,页面加载嘎嘎快,但结果却是用户反馈进入网站很卡,究其原因,这是由 Chrome DevTools 的局限性导致的:
-
网络环境差异:使用
Chrome DevTools是内网访问,往往网络环境很好,而用户的网络环境就很复杂,在偏远地区或者电梯、地铁等弱网环境体验会更差。 - 访问方式不同:调试工具和真机有一定的差距。
- 访问设备有限:测试机观察到的首屏时间机型有限,而真实用户的手机机型五花八门。
所以通过 Chrome DevTools 采集到的数据是不够准确的。所以我们需要通过添加相关代码进行采集,然后把采集到的数据上报到服务器中,这样就能获取大量用户的首屏时间数据。
采集方式一般有两种,手动采集和自动化采集。
2、手动采集
手动采集一般是通过埋点的方式来实现:
- 比如是电商网站首页,需要在导航栏、搜索框、商品头图等内容加载完毕的位置打上点。
- 如果是一个列表页,需要根据各个机型下的首屏位置,计算出一个平均的首屏位置,打上点。
- 如果首屏仅仅是一张图片,则需要在图片加载完成之后,打上点。
优点:
- 灵活性强,可以根据网页的特点随时改变打点策略,以保证首屏时间采集的准确性。
- 去中心化,各个业务部分自己打自己点即可,自行采集和维护。
缺点:
- 通用性差,各个业务要自己去设计打点方案。
- 和业务代码耦合在一起,维护性差,而且随着业务的变化,打点代码也需要调整,较为麻烦。
- 依赖人,不同人对首屏的理解不一样,导致不同人采集的结果有差异,还需要花时间和成本去校正,或者忘记打点。
3、自动化采集
自动化采集就是指插入一段通用代码进行自动化采集。
优点:
- 通用性强,多个业务线都可用,使用和接入简单。
缺点:
- 无法满足业务的个性化需求。
自动化采集对于不同的场景,采集方案也不一样:
- 对于服务端渲染
SSR来说,客户端拿到的就是拼接好的html字符串,所以直接采集DOMContentLoaded的时间即可。 - 对于客户端渲染的
SPA应用来说,DOMContentLoaded的时间并不一定准确,因为里面的内容开始只有一个容器<div id="app"></div>,后续内容是通过js动态渲染出来的,而用户需要看到完整的首屏实际内容,才能算首屏加载完成了。
那么,如何准确采集单页面(SPA)应用的首屏时间呢?
4、单页面(SPA)应用的首屏采集方案
首先先了解下单页应用的渲染大概流程:
- 输入网址,从服务器拿到
index.html文件; - 浏览器使用
html解析器解析html文件,并加载css、js等资源。 - 执行
js代码,初始化框架Vue/React/Angular,执行里面相关生命周期钩子,使用xhr/axios请求数据,并渲染 DOM 到页面上。
那么,我们的核心就是需要知道,渲染 DOM 到页面上的时间。以 Vue 框架为例,它有一个 mounted(Vue2 Options API)、onMounted(Vue3 Composition API ) 钩子,可以拿到 DOM 加载的时间,那么我们是不是能利用这个钩子来进行首屏时间的采集呢?
显然是不行的,这样做有如下缺点:
- 如果页面数据是通过请求异步拿到并渲染到页面上,
mounted采集的首屏时间就不准确了,如果要知道准确的时间,需要等请求完成的时间点进行采集,这样会侵入业务代码,违背了通用性,再说如果有多个请求抽离在各个地方,还需要用类似Promise.all进行整合,还是需要修改业务代码。 - 如果首页是一张图片,而
mounted的时间,图片内容可能并没有加载完,用户也看不到内容。
5、使用 MutationObserver 采集首屏时间
所以,我们应该采用 MutationObserver 进行采集。它能监听 DOM 树的更改并执行相关的回调。核心的统计思路就是:在页面初始化时,使用 MutationObserver 监听 DOM 元素,当其发生变化时,程序会标记变化的元素,并记录时间点和分数,存储到数组中,当达到如下条件时,说明首屏渲染已经结束:
- 计算时间超过
30s还没结束。 - 计算了
4次且1s内分数不再变化。 - 计算了
9次且分数不再变化。
统计分数过程如下:
- 递归遍历 DOM 元素及其子元素,根据元素层级设定元素权重。层级越深的元素最接近用户看到的内容,权重也就越高。比如第一层权重为
1,渲染完成得1分,没增加一层权重增加0.5,第三层的权重为3.5,也就是渲染完成得3.5分。
最终,我们拿到一个记录了时间点和分数的数组,然后通过数组的后一项 - 数组前一项求出元素分数变化率,找到变化率最大点的分数对应的时间,即为首屏时间。
那这样算出来的首屏时间是否准确呢?其实不然,像我们之前说的首屏为一张图片的情况,就采集的不准。
所以对于图片来说,我们需要拿到页面中所有的 img,其来源主要有两方面:
-
img标签:通过拿到 dom 节点,判断其nodeName.toUpperCase === 'IMG'。 - CSS 背景中的图片
background: url("https://static.xxx.png")。可以通过如下方式来拿到:
if (dom.nodeName.toUpperCase !== 'IMG') {
const domStyle = window.getComputedStyle(dom);
const imgUrl = domStyle.getPropertyValue('background-image') || domStyle.getPropertyValue('background');
}
拿到图片的 url 之后,通过 performance.getEntriesByName(imgUrl)[0].responseEnd 获取图片的加载时间,然后拿到图片最长的加载时间和之前变化率最大点的分数对应的时间进行对比,哪个更长哪个就是最终的首屏时间。
小结
![]()
- 首屏时间会受用户设备、网络环境的影响,使用
Chrome DevTools拿到的首屏时间存在偏差。 - 手动采集方案较为灵活,能满足个性化需求,去中心化,但没有自动采集通用性好,会跟业务代码耦合,接入成本也更高,会受人为影响,所以一般都会选择自动化采集方案。
- 采集时,服务端 SSR 应用和单页 SPA 应用的采集有很大不同,SSR 应用只需要采集
DOMContentLoaded时间即可,而单页应用则需要使用MutationObserver监听 DOM,并设置元素权重,统计每个元素的分数和时间,最终拿到变化率最大的分数及时间点。 - 计算出所有图片的加载时间,与变化率最大的分数的时间进行比较,更大的作为最终的首屏时间。