# 白屏/首屏时间指标及采集

# 首屏时间相关指标

# FP(First Paint)

页面上渲染出第一个像素点的时间,一般在 HTML 解析完成或者解析了一部分之后触发,FP 事件触发时间可以任务就是白屏时间

# FCP(First Contentful Paint)

首次内容绘制,即浏览器中第一个内容被渲染完成的时间点,这个内容可以是文本、图片等等。良好的FCP应该控制在1.8s内。

# FMP(First Meaningful Paint)

有意义的内容被首次绘制出的时间,但是 “有意义” 的内容没有一个统一的标准,所以此指标并不常用。根据 MDN 的说法,FMP 已被弃用,取而代之的是LCP。First Meaningful Paint - MDN Web Docs Glossary: Definitions of Web-related terms | MDN (mozilla.org) (opens new window)

# LCP(Largest Contentful Paint)

从页面开始加载到最大内容元素加载完成的时间。最大的内容涂料 (w3c.github.io) (opens new window)

# DCL(DOMContentLoaded )

当 HTML 文档完全解析,且所有延迟脚本下载和执行完毕后,会触发 DOMContentLoaded 事件。它不会等待图片、子框架和异步脚本等其他内容完成加载。

# L(onLoad)

DOM 解析并渲染完成的时间。

一般来说,我们可以认为:

  1. 白屏时间 = FP
  2. 首屏时间 = FCP

# 首屏时间的手动采集

手动采集首屏时间,一般来说是通过埋点的方式进行的。根据不同的业务需求,我们打上不同的点。例如:

  1. 对于淘宝商品详情页,首屏包含商品首图、商品信息、购物车等,我们就可以在这些内容加载完毕的地方打上点
  2. 对应抖音直播页面,主要的核心内容是直播框,我们就可以在直播框加载完成的位置打上点

通过获取页面开始加载时间和我们自定义的结束位置时间,我们可以计算差值来获取首屏时间。

手动采集和业务需求绑定,并不通用,但个性化较强

# 自动化采集首屏时间

自动化采集首屏时间,是通过引入一段通用的代码来做首屏时间来自动化采集。引入过程中除了一些必要的配置,不需要做其他的事情。

缺点在于无法满足某些个性化需求。

# 服务端渲染应用采集方法

首先我们需要知道页面渲染的过程:

对于服务端渲染来说,HTML文档加载并解析完成的时间点就是首屏时间。采集这个时间点,我们可以采用浏览器提供的 DOMContentLoaded API 来实现,在 chrome 的调试工具中我们可以看到 DOMContentLoaded 的具体数值

当页面中的元素被加载和解析完成之后,就会触发 DOMContentLoaded 事件,我们可以记录下此时的时间,减去页面初始化的时间 fetchStart 即可获取到首屏时间。

# 单页面应用的采集方法

# 单页面应用是否可以使用 performance 计算

单页面应用是否可以使用 performance 计算?这点各个技术社区中存有争议,笔者并没有找到一个确切的答案,个人目前认为是不准确的。可以参考阿里云团队前端技术专家彭伟春在2018年 GMTC 大会上关于《大前端时代前端监控的最佳实践》的演讲(链接附在文末)。其中提到了对于某个 SPA 应用的进行首屏时间采集时,使用 performance API 采集到的首屏时间是 1106 ms,而实际的首屏时间却是 1976ms。

造成这一差异的原因可能是由于 SPA 单页面应用的特性,用户在请求页面时,会先加载 index.html 页面,加载完成之后,就会触发 DOMContentLoaded 和 load 事件,而此时页面只是空白页,根本不是真正的首屏时间。 在加载完成之后,会加载 js 脚本,并通过 Ajax 异步请求数据并渲染页面,此时首屏才算渲染完成。

# FMP/LCP 度量 SPA 应用的首屏时间

FMP 在上文中已经提到过,即页面上首次绘制出有意义的元素的时间。对于 SPA 应用来说,index.html 文件加载完成之后,由于页面完全空白,并没有任何有意义的元素被渲染出,所以并不会触发 FMP。当获取到异步请求的数据并渲染出有意义的元素之后,才会真正触发 FMP。所以使用 FMP 来计算 SPA 应用的首屏时间理论上来说是可行的。我们也可以将 FMP 理解为主要内容的可见时间。但 FMP 的 “有意义” 内容很难界定,该指标的定义依赖于特定于浏览器的实现细节,这意味着它无法标准化,并且尚未在所有网络浏览器中实现

LCP 是页面中最大元素被渲染出的时间,页面加载过程中的最大绘制很可能从用户的角度来看意味着一个有意义的事件,他的标准更加统一。FMP在 lighthouse 中已被弃用,目前 W3C 更加推荐开发者使用 LCP 去替代 FMP

# 使用 MutationObserver 计算 SPA 应用的首屏时间

首先我们提出一个猜想,我们是否可以认为页面上元素增量最大的时间点就是页面主要元素被渲染出的时间点,即首屏时间。

这种猜想看似合理,其实是不完善的。页面上的元素种类很多,有些元素并没有实际意义,也有些元素是不可见的,这些元素被渲染出来,对于首屏来说没有任何价值。正因如此,不同种类的元素应该有不同的权重,我们在考虑元素增量的时候,需要加权考虑。

所以,在页面加载时,我们使用 MutationObserver 监控 DOM 元素的变化,当元素变化时,程序会标记变化的元素,同时记录此时的时间点和分数,存储到数组中。加载完毕之后,通过对所收集到的数据的分析,找出分数最大的时间点,即可认为该时间点为首屏时间。具体流程如下:

从图中我们可以看到,元素权重是和元素的层级相关联的,具体来说,层级越深的元素,其权重越大。究其原因,是因为越往内层的元素越接近真实的首屏内容,而越往外的元素,则更接近没有实际内容的框架层,如 body 等。

相关代码:

  1. 初始化监听

    initObserver() {
      try {
        if (this.supportTiming()) {
          this.observer = new MutationObserver(() => {
            let time = Date.now() - performance.timing.fetchStart;
            let bodyTarget = document.body;
            if (bodyTarget) {
              let score = 0;
              score += calculateScore(bodyTarget, 1, false);
              SCORE_ITEMS.push({
                score,
                t: time
              });
            } else {
              SCORE_ITEMS.push({
                score: 0,
                t: time
              });
            }
          });
        }
    
        this.observer.observe(document, {
          childList: true,
          subtree: true
        });
    
        if (document.readyState === "complete") {
          this.mark = 'readyState';
          this.calFinallScore();
        } else {
          window.addEventListener(
            "load",
            () => {
              this.mark = 'load';
              this.calFinallScore();
            },
            true
          );
          window.addEventListener(
            'beforeunload',
            () => {
              this.mark = 'beforeunload';
              this.calFinallScore();
            },
            true
          )
          const that = this;
          function listenTouchstart() {
            if(Date.now() > 2000) {
              that.calFinallScore();
              this.mark = 'touch';
              window.removeEventListener('touchstart', listenTouchstart, true);
            }
          }
          window.addEventListener(
            'touchstart',
            listenTouchstart,
            true
          )
        }
      } catch (error) {}
    }
    
    
  2. 计算分数

    function calculateScore(el, tiers, parentScore) {
      try {
        let score = 0;
        const tagName = el.tagName;
        if (
          "SCRIPT" !== tagName &&
          "STYLE" !== tagName &&
          "META" !== tagName &&
          "HEAD" !== tagName
        ) {
          const childrenLen = el.children ? el.children.length : 0;
          if (childrenLen > 0)
            for (let childs = el.children, len = childrenLen - 1; len >= 0; len--) {
              score += calculateScore(childs[len], tiers + 1, score > 0);
            }
          if (score <= 0 && !parentScore) {
            if (!(el.getBoundingClientRect && el.getBoundingClientRect().top < WH))
              return 0;
          }
          score += 1 + 0.5 * tiers;
        }
        return score;
      } catch (error) {}
    }
    
  3. 计算出首屏时间

    let fmps = getFmp(SCORE_ITEMS);
    let record = null;
    for (let o = 1; o < fmps.length; o++) {
      if (fmps[o].t >= fmps[o - 1].t) {
        let l = fmps[o].score - fmps[o - 1].score;
        (!record || record.rate <= l) &&
          (record = {
            t: fmps[o].t,
            rate: l,
          });
      }
    }
    

# 附:传统性能指标 Performance

Performance 接口可以获取到当前页面中与性能相关的信息

performance 参数解析

startTime:有些浏览器实现为navigationStart,代表浏览器开始unload前一个页面文档的开始时间节点。比如我们当前正在浏览baidu.com,在地址栏输入google.com并回车,浏览器的执行动作依次为:unload当前文档(即baidu.com)->请求下一文档(即google.com)。navigationStart的值便是触发unload当前文档的时间节点。如果当前文档为空,则navigationStart的值等于fetchStart。

redirectStartredirectEnd:如果页面是由redirect而来,则redirectStart和redirectEnd分别代表redirect开始和结束的时间节点;

unloadEventStartunloadEventEnd:如果前一个文档和请求的文档是同一个域的,则unloadEventStart和unloadEventEnd分别代表浏览器unload前一个文档的开始和结束时间节点。否则两者都等于0;

fetchStart是指在浏览器发起任何请求之前的时间值。fetchStart 时代表浏览器已经做好了发起网络请求的准备。在fetchStart和domainLookupStart之间,浏览器会检查当前文档的缓存;

domainLookupStartdomainLookupEnd分别代表DNS查询的开始和结束时间节点。如果浏览器没有进行DNS查询(比如使用了cache),则两者的值都等于fetchStart;

connectStartconnectEnd分别代表TCP建立连接和连接成功的时间节点。如果浏览器没有进行TCP连接(比如使用持久化连接webscoket),则两者都等于domainLookupEnd;

secureConnectionStart:可选。如果页面使用HTTPS,它的值是安全连接握手之前的时刻。如果该属性不可用,则返回undefined。如果该属性可用,但没有使用HTTPS,则返回0;

requestStart代表浏览器发起请求的时间节点,请求的方式可以是请求服务器、缓存、本地资源等;

responseStartresponseEnd分别代表浏览器收到从服务器端(或缓存、本地资源)响应回的第一个字节和最后一个字节数据的时刻;

domLoading代表浏览器开始解析html文档的时间节点。我们知道IE浏览器下的document有readyState属性,domLoading的值就等于readyState改变为loading的时间节点;

domInteractive代表浏览器解析html文档的状态为interactive时的时间节点。domInteractive并非DOMReady,它早于DOMReady触发,代表html文档解析完毕(即dom tree创建完成)但是内嵌资源(比如外链css、js等)还未加载的时间点;

domContentLoadedEventStart:代表DOMContentLoaded事件触发的时间节点: 页面文档完全加载并解析完毕之后,会触发DOMContentLoaded事件,HTML文档不会等待样式文件,图片文件,子框架页面的加载(load事件可以用来检测HTML页面是否完全加载完毕(fully-loaded))。

domContentLoadedEventEnd:代表DOMContentLoaded事件完成的时间节点,此刻用户可以对页面进行操作,也就是jQuery中的domready时间;

domComplete:html文档完全解析完毕的时间节点;

loadEventStartloadEventEnd分别代表onload事件触发和结束的时间节点

参考资料:

  1. GMTC北京2018_全球大前端技术大会_ - 前端时代前端监控的最佳实践 (opens new window)

  2. 前端vite+vue3——可视化页面性能耗时指标(fmp、fp)-阿里云开发者社区 (aliyun.com) (opens new window)

  3. 前端性能监控方案(首屏、白屏时间等) - 掘金 (juejin.cn) (opens new window)