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

共 9412字,需浏览 19分钟

 ·

2022-02-27 16:29

作者:Sunshine_Lin

简介:「前端之神」的号主江湖人称林三心,现已有100+篇原创文章,全网粉丝高达1w+,面试过超过100+个前端程序员,全网获赞2w+,全网阅读量播放量超过60w,更是B站「面试进阶成为大佬」系列视频的Up主。喜欢分享Vue,React,Typescript等高级前端知识。

来源:SegmentFault  思否社区 


前言



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


背景



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


  • 白屏时间
  • 首屏时间

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


白屏时间



是什么?


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


怎么算?


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

因此,我们通常认为浏览器开始渲染 <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


总结



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




点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -


浏览 19
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报