npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

fmp-tti

v1.1.4

Published

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

Downloads

80

Readme

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