仓库完整代码:first-screen-paint,若是来过,期待留下你的一颗小星星~css
1. First Paint(FP)node
First Paint
的定义是渲染树首次转变为屏幕像素的过程,咱们用FP time
来表达首次渲染时间。在FP
以前咱们看见的屏幕是空白的,那么FP time
也可理解为白屏时间。如何计算呢?git
if (window.performance) {
let pf = window.performance;
let pfEntries = pf.getEntriesByType('paint')
let fp = pfEntries.find(each => each.name === 'first-paint')
console.log('first paint time: ', fp && fp.startTime)
}
复制代码
2. First Contentful Paint(FCP):github
FCP
定义的是从页面加载到屏幕上首次有渲染内容的过程,这里的内容能够是文本、图像、svg
元素和非白色canvas
元素。在下图加载时间线中,图二是FCP
的时间点: 咱们用
FCP time
来表达内容首次渲染时间。如何计算呢?web
if (window.performance) {
let pf = window.performance;
let pfEntries = pf.getEntriesByType('paint')
let fp = pfEntries.find(each => each.name === 'first-contentful-paint')
console.log('first paint time: ', fp && fp.startTime)
}
复制代码
须要区别于FP,总有FP time ≤ FCP time。canvas
3. First Meaningful Paint(FMP)api
FMP
定义的是从页面开始加载到渲染出主要内容的过程,这个“主要内容”的定义依赖于各浏览器中的实现细节,所以它并无做为一个标准化的指标。在Chrome的Lighthouse面板中咱们能够看到这个指标: 浏览器
4. Largest Contentful Paint(LCP)markdown
FMP
的范围很差界定,但LCP
的范围是恒定的,它定义的是页面开始加载到渲染出(视口内)最大内容(文本或图像等)的过程。以下图加载时间线: app
第一个示例中,Instagram logo是视口中的最大内容,第二个示例中,绿色的文本是视口中的最大内容块。咱们用
LCP time
表达最大内容渲染时间,如何计算呢?
new PerformanceObserver(list => {
let entries = list.getEntriesByType('largest-contentful-paint');
entries.forEach(item => {
console.log('largest contentful pain time: ', item.startTime)
})
}).observe({ entryTypes: ['largest-contentful-paint'] });
复制代码
咱们这里定义的首屏是指页面无滚动的状况下,从开始加载到视窗第一屏内容渲染完成的过程,遵循上面几个概念的定义,咱们能够称它为 last contentful paint
,亦或first screen paint
更贴切一些。在本文,咱们就把首屏渲染时间叫作first screen paint time(FSP time
),要如何来统计呢?
先考虑最简单的场景:咱们的页面是纯静态文本型的,即首屏里面没有图片,内容是静态文本。
咱们要先解决一个问题:如何界定哪些元素是属于屏内的?
1. getBoundingClientRect
getBoundingClientRect
用于获取某个元素相对于视窗的位置,理论上咱们只要计算每个元素的位置信息,结合视窗的高度信息,咱们就能判断元素是否属于屏内。
但在真实状况下,一个页面dom的数量是很庞大的,大量的dom操做自己就会影响整个页面的性能!况且,getBoundingClientRect
会引发页面重排(what forces reflow/layout),这并非一个理想的方案;
2. IntersectionObserver + MutationObserver
IntersectionObserver
经过启动一个观察器,以一种异步的方式检查目标元素是否出现于视窗(viewport)中,它返回的数据里面包含了两个重要的信息:
接下来咱们须要给每个元素添加一个intersection观察器,MutationObserver
能够帮助咱们,它提供了监视dom树变动的能力,咱们使用它监视document
根节点的子树的变化,为新增的每个子节点注册一个IntersectionObserver
,参考以下代码:
// 注册可视性监听器
const isObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// 屏内元素
if (entry.intersectionRatio > 0) {
// 记录节点及其时间,这里也可使用人工打点的方式:performance.now()
console.log(`${entry.target}: ${entry.time}`);
}
});
});
// 注册DOM树变动监听器
const muObserver = new MutationObserver((mutations) => {
if (!mutations) return;
mutations.forEach((mu) => {
if (!mu.addedNodes || !mu.addedNodes.length) return;
mu.addedNodes.forEach((ele) => {
// 只对元素节点进行监听
if (ele.nodeType === 1) {
// 添加可视性变化监听器
isObserver.observe(ele);
}
});
});
});
// 监听document的子树变化
muObserver.observe(document, {
childList: true,
subtree: true
});
复制代码
更完整的代码参考:first-screen-paint
场景2:首屏包含图片资源,多是图片元素或背景,须要计算加载最慢那张图片资源的耗时
问题1:图片资源是异步加载的,如何获取资源的请求耗时?
前文咱们介绍了获取LCP time的方法,用相似的方式,咱们也能获取图片资源的耗时,使用PerformanceObserver
api监听资源的加载耗时,它返回的数据里面包含了几个重要的信息:
responseEnd
于startTime
的差值;const pfObserver = new PerformanceObserver((list) => {
const entries = list.getEntriesByType('resource');
entries.forEach((item) => {
// 各类资源的耗时
// 首屏图片资源白名单:imgUrlWhiteList = []
console.log(`${item.name: ${item.duration}}`);
});
});
// 设定性能监听类别:资源
pfObserver.observe({ entryTypes: ['resource'] });
复制代码
问题2:上面代码中咱们监听了全部资源的请求,如何取出首屏的图片资源请求?
MutationObserver
或者IntersectionObserver
监听器中直接操做dom读取img
的src
或者data-src
属性,把图片URL保存起来;background-image
的值;场景3:首屏内容是动态fetch的,甚至fetch的是图片资源,就如商城首页?
数据是动态fetch的,若是是纯文本数据,无图片资源。咱们的DOM树变动监听器能够监听到数据返回以后的渲染状况,渲染过程会收集这些节点的可见性变化时间(这个时间确定是在fetch数据返回时间点以后的);若是渲染的是图片资源,那么就进入了上一个处理图片资源的场景。
1. 首屏内容还在加载中,用户触发了页面滚动?
页面滚动以后,第二屏的内容就会出如今视窗,本来属于首屏的内容(部份内容可能并未完成渲染)却没在视窗中。那么,按照如上的统计方式,就会统计到当前处于视窗内容的渲染时间,这可能就是一个“偏差”。
咱们须要一个共识:在首屏内容彻底渲染以前页面触发了滚动,说明页面已是一个可交互的状态,这种状况下,咱们认为,用户触发滚动时那一帧的内容,已是用户和开发者双方都能接受的首屏内容。基于这个前提,咱们的处理方式是:
在页面滚动时,加一个锁,中止监听后续内容的变动,以初次滚动的时间点为时间界线,统计在此时间点前发出的(依据startTime
)全部资源的请求耗时和dom树节点的渲染时间;
2. 在场景3下,首屏内容未加载完,用户触发了页面滚动?
在本测试demo中,页面的主体内容是img元素,按照LCP
(lagest contentful paint)的定义,LCP time
会返回这张图片渲染的时间;而咱们的首屏内容亦是这张图片,那么咱们的FSP time
应该基本等于LCP time
,在下面截图中,也基本验证了这一点!
最后,对于上面提到的几个问题,各位读者有任何见解也可在评论区留言~
仓库地址:first-screen-paint,欢迎提issue~
参考: