6

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

 2 years ago
source link: https://segmentfault.com/a/1190000041419030
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

  • 白屏时间
  • 首屏时间

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

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

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

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

  • 浏览器支持 performance.timing

    <head>
    <title>Document</title>
    </head>
    <script type="text/javascript">
    // 白屏时间结束点
    var firstPaint = Date.now()
    var start = performance.timing.navigationStart
    console.log(firstPaint - start)
    </script>
  • 浏览器不支持 performance.timing

    <head>
    <title>Document</title>
    <script type="text/javascript">
      window.start = Date.now();
    </script>
    </head>
    <script type="text/javascript">
    // 白屏时间结束点
    var firstPaint = Date.now()
    console.log(firstPaint - window.start)
    </script>

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

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

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

为什么不直接用nextTick?

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

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

<body>
    <div>
      <div>1</div>
      <div>2</div>
    </div>
</body>

以上DOM结构的分数为:

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

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

首屏时间实践

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

  • index.html:html页面

    <!DOCTYPE html>
    <html lang="en">
    <head> </head>
    <body>
      <div>
        <div>
          <div>1</div>
          <div>2</div>
        </div>
        <div>3</div>
        <div>4</div>
      </div>
      <ul id="ulbox"></ul>
    </body>
    <script src="./computed.js"></script>
    <script src="./request.js"></script>
    </html>
  • 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 < list.length; i++) {
      const li = document.createElement('li')
      li.innerText = list[i]
      fragment.appendChild(li)
    }
    ulbox.appendChild(fragment)
    }
    
    // 模拟对列表进行轻微修改
    const addList = async () => {
    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 <= differ) {
      data = {
        time: observerData[i].time,
        rate: differ
      }
    }
  }
  return data
}

let timer = null

function unmountObserver(delay) {
  if (timer) return
  timer = setTimeout(() => {
    // 输出首屏时间
    console.log(getFirstScreenTime())
    // 终止MutationObserver的监控
    observer.disconnect()
    observer = null
    clearTimeout(timer)
  }, delay)
}

计算出首屏时间 1020ms

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

我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜欢前端,想学习前端,那咱们可以交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】

image.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK