fmp-tti

1.1.4 • Public • Published

FMP-TTI

自动化无埋点方式计算页面FCP/FMP/TTI时间

在前端性能监控方面,虽然我们可以通过 performance timing api 获得一些浏览器提供的关键节点时间,来数值化的衡量一个页面的性能概况。但这个只是从资源加载解析的角度来看,跟实际用户视觉体验上的页面加载可能不一致。因此我们根据页面加载过程给予用户的视觉体验反馈,定义了以下几个性能指标:

用户体验 性能指标 定义
页面是否正在正常加载 首次内容绘制(FCP) 首屏部分首次渲染出文本、图片的时间点
页面是否已加载足够内容? 首次有意义绘制(FMP) 首屏部分主要元素渲染出来的时间点
页面是否已经可以操作了 可交互时间(TTI) FMP 及 DOMContentLoaded 之后,首次 JS 空闲的时间点,此时页面可以响应用户交互

在有了性能指标后,常见的方案是进行埋点上报。但这种方案上报逻辑跟业务代码强耦合,接入成本较高。在分析了页面加载流程及各个性能指标点对应关系后,我们做了如下定义:

  • 首次内容绘制(FCP):首屏部分首次渲染出文本、图片的时间点
  • 首次有意义绘制(FMP):首屏部分主要元素渲染出来的时间点
  • 可交互时间(TTI):FMP 及 DOMContentLoaded 之后,首次 JS 空闲的时间点,此时页面可以响应用户交互

明确定义后,我们可以采用 MutationObserver 来监听DOM变更,从而分析出各个节点的时间。

使用方式

安装组件

npm i -S fmp-tti

根据需要设置页面 TTI 超时时间阈值

// 需要在引入组件前设置,默认值为 10000(10秒)
window.TTI_LIMIT = 10000;

<head> 标签中, css 加载前引入组件

  • ES6 方式引入
    import FT from 'fmp-tti';
  • 外链方式引入
    <script src="./fmp-tti/index.iife.js"></script>

达到超时时间阈值后,系统会返回检测结果,值为从 navigationStart 到对应节点的耗时。

FT.then(({ fcp, fmp, tti }) => {
    console.log('首次内容绘制(FCP) - %dms', fcp);
    console.log('首次有意义绘制(FMP) - %dms', fmp);
    console.log('可交互时间(TTI) - %dms', tti);
});

如果系统已经检测到 TTI 时间,但是还没达到超时时间阈值,此时用户关闭了页面,这时组件会将检测结果存放在 localStorage 缓存中,下次打开时可以通过 FT.then() 方法来获取上次检测结果。

FT.last(({ fcp, fmp, tti }) => {
    console.log('上次检测结果', { fcp, fmp, tti });
});

各指标点计算规则

通过 MutationObserver 监听首屏部分 DOM 树变化,将每次变更量化成渲染得分,根据渲染得分值变化情况来判断当前页面的加载情况。

渲染得分计算方式

遍历变更的节点列表,依次判断各个 DOM 节点是否有效

判断条件:

  • 挂载在 body 节点下
  • 位于 1屏 范围之内
  • 图片节点存在 src 属性时有效
  • 非图片节点节点需宽高不为 0,且存在 textContent 或者 backgroundImage 时判断为有效

每个有效节点计 1分

指标点计算规则

性能指标 定义 计算方式
首次内容绘制(FCP) 首屏部分首次渲染出文本、图片的时间点 取总渲染得分首次大于 0 的点
首次有意义绘制(FMP) 首屏部分主要元素渲染出来的时间点 选取得分变化最大的区间中得分变化最大的点作为FMP
可交互时间(TTI) FMP 及 DOMContentLoaded 之后,首次 JS 空闲的时间点,此时页面可以响应用户交互 FMP 之后,出现连续 1 秒没有长任务的时间点起点

FMP 的计算过程中,我们采用先判断得分变化最大的区间,再判断该区间中得分变化最大的点,而不是直接判断得分变化最大的点。

当渲染得分变化如上图所示时,页面渲染过程中有两次大的变更,第二次变更大于第一次变更,是目标的 FMP 点。但是由于第一次变更大于第二次变更中的任何一次子变更,如果采用直接判断得分变化最大的方法时,程序判定的 FMP 会是第一次变更,与实际情况不符。因此我们采用现在的区间判定方法。

FMP 判定的区间为不超过 50ms。

对于 TTI 的计算,我们采用的方法是判断在 FMP 和 DOM Ready 后, JS 线程空闲且接下来 1S 内不存在 LongTask(JS运行时间超过50ms) 的时间点。具体计算时采用比较两次 setTimeout(1) 时间间隔的方式来判断是否存在 LongTask,且会通过类似 TCP 拥塞控制机制的方式来动态调整 setTimeout 的延迟时间,从而减少CPU消耗。

VS Code中可以使用 Markdown Preview Mermaid Support 插件查看流程图

总的算法整体流程如下:

graph TD;
    start((开始)) --> step_start[获取navigationStart时间点]
    step_start --> step_observe[MutationObserver监听]
    step_observe -- 监听到新添DOM节点 --> step_change(获取新添的DOM节点)
    step_change -- NodeList --> step_stat>计算渲染得分]
    step_stat --> check_fcp{判断是否是FCP}
    check_fcp -- 是 --> check_fmp{判断是否是FMP}
    check_fmp -- 是 --> step_tti>异步计算TTI时间点]
    step_tti --> check_out{判断是否达到时间阈值}
    check_fcp -- 否 --> check_out
    check_fmp -- 否 --> check_out
    check_out -- 否 --> step_observe
    check_out -- 是 --> step_callback(触发回调)
    step_callback --> stop((结束))

计算渲染得分流程:

graph TD;
    start((开始)) -- 监听到的DOM变更 --> nodelist(NodeList)
    nodelist  --> step_loop[遍历列表]
    step_loop -- 完成遍历 --> step_callback(输出总得分)
    step_callback --> stop((结束))
    step_loop -- 未完成遍历 --> check2{判断节点是否有效}
    check2 -- 无效 --> ignore[忽略并继续遍历]
    check2 -- 有效 --> step_score[计算渲染得分]
    step_score --> check_children{查找子元素}
    check_children -- 存在子元素 --> step_children[获取子元素]
    step_children --> nodelist
    check_children -- 不存在子元素 --> ignore
    ignore --> step_loop

对于 TTI 的计算,我们采用的方法是判断在 FMP 和 DOM Ready 后, JS 线程空闲且接下来 1S 内不存在 LongTask(JS运行时间超过50ms) 的时间点

TTI 计算流程流程:

graph TD;
    start((开始)) --> fmp(FMP时间点)
    fmp --> setp_delay[检测线程空闲的时间点]
    setp_delay -- setTimeout 1 --> check_delay1{间隔>50ms}
    check_delay1 -- 是,线程繁忙 --> setp_delay
    check_delay1 -- 否,线程空闲 --> step_busy[检测线程空闲时长]
    step_busy --  setTimeout 1 --> check_delay2{间隔>50ms}
    check_delay2 -- 否,线程空闲 --> check_empty2{连续空闲时间>1S}
    check_delay2 -- 是,线程繁忙 --> check_empty1{连续空闲时间>1S}
    check_empty1 -- 否 --> setp_delay
    check_empty1 -- 是 --> tti(标记线程空闲的时间为待选TTI时间)
    check_empty2 -- 是 --> tti
    check_empty2 -- 否 --> step_busy
    tti -- 对齐 DOM Ready 时间 --> stop((结束))

setTimeout 1 可能会导致大量循环,对性能影响较大,可以根据每次响应间隔时间来调整定时器间隔,优化后的 TTI 计算流程流程:

graph TD;
    start((开始)) --> fmp(FMP时间点) --> step_0[设置初始定时器间隔时间 $T = 1ms]
    step_0 --> check_ready{连续空闲时间 > 1s}
    check_ready -- 是 --> info_3(标记开始空闲的时间为待选 TTI时间点) -- 对齐 DOM Ready 时间 --> stop((结束))
    check_ready -- 否 --> settimeout[标记当前时间与定时器触发的时间间隔值为 $D]
    wait_next[准备开始下一次检测] --> check_ready
    settimeout --> info_0(判断是否线程繁忙) --> check_50{$D > 50ms ?}
    check_50 -- 是 --> info_2(线程繁忙,缩短定时器时间间隔) --> step_4[$T = $T / 2] --> wait_next
    check_50 -- 否 --> info_1(线程空闲,根据响应偏差情况调整定时器间隔) --> check_10{$D - $T < 10 ?}
    check_10 -- 是 --> check_16{$D < 16ms ?}
    check_10 -- 否 --> wait_next
    check_16 -- 是 --> step_1[$T = $T * 2] --> wait_next
    check_16 -- 否 --> check_25{$D < 25ms ?}
    check_25 -- 是 --> step_2[$T = $T + 1] --> wait_next
    check_25 -- 否 --> step_3[$T = 25ms] --> wait_next

Readme

Keywords

none

Package Sidebar

Install

npm i fmp-tti

Weekly Downloads

33

Version

1.1.4

License

MulanPSL-2.0

Unpacked Size

46.3 kB

Total Files

8

Last publish

Collaborators

  • xingqiao