Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

权重定位 FMP #32

Open
zwwill opened this issue Apr 12, 2019 · 1 comment
Open

权重定位 FMP #32

zwwill opened this issue Apr 12, 2019 · 1 comment

Comments

@zwwill
Copy link
Owner

zwwill commented Apr 12, 2019

image

什么是FMP?

可能大家对「白屏时间」这个名词并不陌生,他是「刀耕火种」年代,我们收集的页面性能指标之一,随着前端工程的复杂化,白屏时间已经没有什么实质性的意义了,取而代之的就是 FMP。

先来介绍几个与之相关的名词。

  • FP(First Paint):首次绘制,标记浏览器渲染任何在视觉上不同于导航前屏幕内容的时间点
  • FCP(First Contentful Paint):首次内容绘制,标记的是浏览器渲染第一针内容 DOM 的时间点,该内容可能是文本、图像、SVG 或者 <canvas> 等元素
  • FMP(First Meaning Paint):首次有效绘制,标记主角元素渲染完成的时间点,主角元素可以是视频网站的视频控件,内容网站的页面框架也可以是资源网站的头图等。

相对于 FP 和 FCP,FMP 是我们前端最常关注的重要性能指标,Google 定义它为「是否有用?」的时间点。然而,「是否有用?」是很难以通用方式界定的,因此,至今依然没有标准的 API 输出。

社区中常有这么几种方式进行「相对准确」的计算 FMP,所谓相对准确,是相对于实际项目而言。

  1. 主动上报:开发者在相应页面的「Meaning」位置上报时间
  2. 权重计算:根据页面元素,计算权重最高的元素渲染时间
  3. 趋势计算:在 render 期间,根据 dom 的变化趋势推算 FMP 值

本文将着重介绍第二种方式。

权重定位

所谓权重,即,将页面的元素以约定的「权重比」遍历出「权重值」最大的某一个或一组 DOM,然后以其「装载时间点」或「加载结束点」作为 FMP 的映射。

权重计算

节点标记

想要对 DOM 节点进行阶段性标记,就得有监听 DOM 变化的能力,庆幸的是,HTML5 赋予了我们这个能力。

MutationObserver,Mutation Events功能的替代品,是DOM3 Events规范的一部分。他可以在指定的 DOM 发生变化时执行回调。

MutationObserver 有三个方法

  • disconnect()

    阻止 MutationObserver 实例继续接收的通知,直到再次调用其observe()方法,该观察者对象包含的回调函数都不会再被调用。

  • observe()

    配置MutationObserver在DOM更改匹配给定选项时,通过其回调函数开始接收通知。

  • takeRecords()

    从MutationObserver的通知队列中删除所有待处理的通知,并将它们返回到MutationRecord对象的新Array中。

global.mo = new MutationObserver(() => { 
    /* callback: DOM 节点设置阶段性标记 */
});

/**
 * mutationObserver.observe(target[, options])
 * target - 需要观察变化的 DOM Node。
 * options - MutationObserverInit 对象,配置需要观察的变化项。
 * 更多 options 的介绍请参考 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserverInit#%E5%B1%9E%E6%80%A7
 **/
global.mo.observe(document, {
  childList: true,  // 监听子节点变化(如果subtree为true,则包含子孙节点)
  subtree: true // 整个子树的所有节点
});

下图粗滤的解析了正常单页面的渲染过程

image

  • 预备阶段:导航阶段,处在连接相应的过程
  • 阶段一:首字节渲染阶段,也是FCP,DOM 树的第一次有效变化
  • 阶段二:基本框架渲染完成
  • 阶段三:获取到数据,渲染到视图上
  • 阶段四:图片加载完成,加载过程不被标记

实际上在第一、第三阶段之间还存在着大量的 DOM 变化,Mutation Observer 事件的触发并不是同步的,而是异步触发的,也就是说,等到当前「阶段」所有 DOM 操作都结束才触发。

Mutation Observer 有以下特点

  • 它等待所有脚本任务完成后,才会运行(即异步触发方式)。
  • 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。
  • 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。

load 事件触发后,各个阶段的 tag 已经被打到标签上了

image

此处以『_ti』昨晚标记 key。

image

在打标记的同时,需要记录下当前的时间节点,备用

// 伪代码
function callback() {
    global.timeStack[++_ti] = performance.now(); // 记时间
    doTag(_ti); // 打标记
}

标记打完后就等 load 的那一刻进行计算反推了。

计算权重值

一般来说

  • 视图占比越大的元素越有可能是主角元素
  • 视频比图片更可能是主角元素
  • svgcanvas 也很重要
  • 其他元素都可以按普通 dom 计算了
  • 背景图片视情况而定
第一步:简单粗暴,按大小计算
// 伪代码
function weightCompute(node){
    let {
        width,
        height,
        left,
        top
    } = node.getBoundingClientRect();
    
    // 排除视图外的元素
    if(isOutside(width, height, left, top)){
        return 0;
    }
    let wts = TAG_WEIGHT_MAP[node.tagName]; // 约定好的权重比
    let weight = width * height * wts; // 直接乘,或者更细粒度的计算 wts(width, height, wts)
    return {
        weight, 
        wts, 
        tagName: node.tagName, 
        ti: node.getAttribute("_ti"),
        node
    };
}
第二步:根据权重值推导主角元素

在我们的约定权重算法下,权重最大的元素即为我们推到的主角元素。

// 伪代码
function getCoreNode(node){
    let list = nodeTraversal(node); // 递归计算每个标记节点的权重值
    return getNodeWithMaxWeight(list); // weight 最大的元素
}
第三步:根据元素类型取时间

不同的元素获取时间的方式并不相同

  • 普通元素:按标记点时间计算
  • 图片和视频:按资源相应结束时间计算
  • 带背景元素:可以以背景资源相应结束时间计算,也可以按普通元素计算
// 伪代码
function getFMP(){
    let coreObj = getCoreNode(document.body),
        fmp = -1;
    let {
        tagName,
        ti,
        node
    } = coreObj;
    
    switch(tagName){
        case 'IMG':
        case 'VIDEO':
            let source = node.src;
            let { responseEnd } = performance.getEntries().find(item => item.name === source);
            fmp = responseEnd || -1;
            break;
        default:
            if(node.style.backgroundImage){
                // 普通元素的背景处理
            }else{
               fmp = global.timeStack[+ti]; 
            }
    }
    return fmp;
}

回归验证

以我们的 demo 页为例,类似的电商网站,我们希望拿到「阶段二」或「阶段三」的时间点作为我们的 FMP 值。

image

因为我们并不希望「主角元素」的背景或者「图片主角元素」的相应时间算在 FMP 的值内,所以,我们将「图片」「视频」等资源元素降级成普通元素计算。

在 Chrome [ Disable cache / Fast 3G ] 条件下我们进行模拟验证。

image

image

计算得到的 FMP 值为 4730.7ms,Chrome Performance 监控的值在 4950ms 左右,误差在 200ms 左右。

如果将限速放开,FMP 的取值将更接近我们希望的「First Meaning Paint」。

@rccoder
Copy link

rccoder commented Apr 13, 2019

如果有浏览器内核支撑计算的话,这里时间就有点类似 U4 的 T2 了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants