首屏时间,你说你优化了,那你倒是计算出给给我看啊!

2024年 4月 15日 105.9k 0

前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心

背景

当我们在做项目的性能优化的时候,优化首屏时间是一个避不过去的优化方向,但是又有多少人想过这两个东西的区别呢:

  • 白屏时间
  • 首屏时间

并且这两个时间的计算方式又有什么区别呢?接下来我就给大家讲一下吧!

白屏时间

是什么?

白屏时间指的是:页面开始显示内容的时间。也就是:浏览器显示第一个字符或者元素的时间

图片图片

怎么算?

我们只需要知道浏览器开始显示内容的时间点,即页面白屏结束时间点即可获取到页面的白屏时间。

因此,我们通常认为浏览器开始渲染标签或者解析完标签的时刻就是页面白屏结束的时间点。

  • 浏览器支持performance.timing

  Document


  // 白屏时间结束点
  var firstPaint = Date.now()
  var start = performance.timing.navigationStart
  console.log(firstPaint - start)
  • 浏览器不支持performance.timing

  Document
  
    window.start = Date.now();
  


  // 白屏时间结束点
  var firstPaint = Date.now()
  console.log(firstPaint - window.start)

首屏时间

是什么?

首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。

图片图片

为什么不直接用生命周期?

有些小伙伴会说:为啥不直接在App.vue的mounted生命周期里计算时间呢?大家可以看看,官网说了mounted执行并不代表首屏所有元素加载完毕,所以mounted计算出来的时间会偏短。

图片图片

为什么不直接用nextTick?

nextTick回调的时候,首屏的DOM都渲染出来了,但是计算首屏时间并不需要渲染所有DOM,所以计算出来的时间会偏长

怎么算?

我们需要利用MutationObserver监控DOM的变化,监控每一次DOM变化的分数,计算的规则为: (1 + 层数 * 0.5),我举个例子:


    
      1
      2
    

以上DOM结构的分数为:

1.5 + 2 + 2.5 + 2.5 = 8.5(分)

图片图片

其实在首屏的加载中,会涉及到DOM的增加、修改、删除,所以会触发多次MutationObserver,所以会统计出不同阶段的score,我们把这些score存放在一个数组observerData中,后面大有用处

首屏时间实践

现在我们开始计算首屏时间吧!

前置准备

  • index.html:html页面


   
  
    
      
        1
        2
      
      3
      4
    
    
    • computed.js:计算首屏时间的文件
    const observerData = []
    
    let observer = new MutationObserver(() => {
      // 计算每次DOM修改时,距离页面刚开始加载的时间
      const start = window.performance.timing.navigationStart
      const time = new Date().getTime() - start
      
      const body = document.querySelector('body')
      const score = computedScore(body, 1)
      // 加到数组 observerData 中
      observerData.push({
        score,
        time
      })
    })
    observer.observe(
      document, {
        childList: true,
        subtree: true
      }
    )
    
    function computedScore(element, layer) {
      let score = 0
      const tagName = element.tagName
      // 排除这些标签的情况
      if (
        tagName !== 'SCRIPT' &&
        tagName !== 'STYLE' &&
        tagName !== 'META' &&
        tagName !== 'HEAD'
      ) {
        const children = element.children
        if (children && children.length) {
          // 递归计算分数
          for (let i = 0; i < children.length; i++) {
            score += computedScore(children[i], layer + 1)
          }
        }
    
        score += 1 + 0.5 * layer
      }
      return score
    }
    • request.js:模拟请求修改DOM
    // 模拟请求列表
    const requestList = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(
            [1, 2, 3,
              4, 5, 6,
              7, 8, 9
            ]
          )
        }, 1000)
      })
    }
    
    const ulbox = document.getElementById('ulbox')
    
    // 模拟请求数据渲染列表
    const renderList = async () => {
      const list = await requestList()
      const fragment = document.createDocumentFragment()
      for (let i = 0; i  {
      const li = document.createElement('li')
      li.innerText = '加上去'
      ulbox.appendChild(li)
    }
    
    (async () => {
      // 模拟请求数据渲染列表
      await renderList()
      // 模拟对列表进行轻微修改
      addList()
    })()

    observerData

    当我们一切准备就绪后运行代码,我们获得了observerData,我们看看它长什么样?

    计算首屏时间

    我们怎么根据observerData来计算首屏时间呢?我们可以这么算:下次分数比上次分数增加幅度最大的时间作为首屏时间

    很多人会问了,为什么不是取最后一项的时间来当做首屏时间呢?大家要注意了:首屏并不是所有DOM都渲染,我就拿刚刚的代码来举例吧,我们渲染完了列表,然后再去增加一个li,那你是觉得哪个时间段算是首屏呢?应该是渲染完列表后算首屏完成,因为后面只增加了一个li,分数的涨幅较小,可以忽略不计

    所以我们开始计算吧:

    const observerData = []
    
    let observer = new MutationObserver(() => {
      // 计算每次DOM修改时,距离页面刚开始加载的时间
      const start = window.performance.timing.navigationStart
      const time = new Date().getTime() - start
      const body = document.querySelector('body')
      const score = computedScore(body, 1)
      observerData.push({
        score,
        time
      })
    
      // complete时去调用 unmountObserver
      if (document.readyState === 'complete') {
        // 只计算10秒内渲染时间
        unmountObserver(10000)
      }
    })
    observer.observe(
      document, {
        childList: true,
        subtree: true
      }
    )
    
    function computedScore(element, layer) {
      let score = 0
      const tagName = element.tagName
      // 排除这些标签的情况
      if (
        tagName !== 'SCRIPT' &&
        tagName !== 'STYLE' &&
        tagName !== 'META' &&
        tagName !== 'HEAD'
      ) {
        const children = element.children
        if (children && children.length) {
          // 递归计算分数
          for (let i = 0; i < children.length; i++) {
            score += computedScore(children[i], layer + 1)
          }
        }
    
        score += 1 + 0.5 * layer
      }
      return score
    }
    
    // 计算首屏时间
    function getFirstScreenTime() {
      let data = null
      for (let i = 1; i < observerData.length; i++) {
        // 计算幅度
        const differ = observerData[i].score - observerData[i - 1].score
        // 取最大幅度,记录对应时间
        if (!data || data.rate  {
        // 输出首屏时间
        console.log(getFirstScreenTime())
        // 终止MutationObserver的监控
        observer.disconnect()
        observer = null
        clearTimeout(timer)
      }, delay)
    }

    计算出首屏时间1020ms

    总结

    我这个计算方法其实很多漏洞,没把删除元素也考虑进去,但是想让大家知道计算首屏时间的计算思想,这才是最重要的,希望大家能理解这个计算思想。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论