你好,我是李兵
上节我们介绍了如何使用Performance,而且我们还提到了性能指标面板中的Main指标,它详细地记录了渲染主线程上的任务执行记录,通过分析Main指标,我们就能够定位到页面中所存在的性能问题,本节,我们就来介绍如何分析Main指标。
不过在开始之前,我们要讲清楚两个概念,那就是Main指标中的任务和过程,在《15 | 消息队列和事件循环:页面是怎么活起来的?》和《加餐二|任务调度:有了setTimeOut,为什么还要使用rAF?》这两节我们分析过,渲染进程中维护了消息队列,如果通过SetTimeout设置的回调函数,通过鼠标点击的消息事件,都会以任务的形式添加消息队列中,然后任务调度器会按照一定规则从消息队列中取出合适的任务,并让其在渲染主线程上执行。
而我们今天所分析的Main指标就记录渲染主线上所执行的全部任务,以及每个任务的详细执行过程。
你可以打开Chrome的开发者工具,选择Performance标签,然后录制加载阶段任务执行记录,然后关注Main指标,如下图所示:
观察上图,图上方有很多一段一段灰色横条,每个灰色横条就对应了一个任务,灰色长条的长度对应了任务的执行时长。通常,渲染主线程上的任务都是比较复杂的,如果只单纯记录任务执行的时长,那么依然很难定位问题,因此,还需要将任务执行过程中的一些关键的细节记录下来,这些细节就是任务的过程,灰线下面的横条就是一个个过程,同样这些横条的长度就代表这些过程执行的时长。
直观地理解,你可以把任务看成是一个Task函数,在执行Task函数的过程中,它会调用一系列的子函数,这些子函数就是我们所提到的过程。为了让你更好地理解,我们来分析下面这个任务的图形:
观察上面这个任务记录的图形,你可以把该图形看成是下面Task函数的执行过程:
function A(){
A1()
A2()
}
function Task(){
A()
B()
}
Task()
结合代码和上面的图形,我们可以得出以下信息:
通过以上介绍,相信你已经掌握了如何解读Main指标中的任务了,那么接下来,我们就可以结合Main指标来分析页面的加载过程。我们先来分析一个简单的页面,代码如下所示:
<html>
<head>
<title>Main</title>
<style>
area {
border: 2px ridge;
}
box {
background-color: rgba(106, 24, 238, 0.26);
height: 5em;
margin: 1em;
width: 5em;
}
</style>
</head>
<body>
<div class="area">
<div class="box rAF"></div>
</div>
<br>
<script>
function setNewArea() {
let el = document.createElement('div')
el.setAttribute('class', 'area')
el.innerHTML = '<div class="box rAF"></div>'
document.body.append(el)
}
setNewArea()
</script>
</body>
</html>
观察这段代码,我们可以看出,它只是包含了一段CSS样式和一段JavaScript内嵌代码,其中在JavaScript中还执行了DOM操作了,我们就结合这段代码来分析页面的加载流程。
首先生成报告页,再观察报告页中的Main指标,由于阅读实际指标比较费劲,所以我手动绘制了一些关键的任务和其执行过程,如下图所示:
通过上面的图形我们可以看出,加载过程主要分为三个阶段,它们分别是:
那么接下来,我就按照这三个步骤来介绍如何解读Main指标上的数据。
我们先来看导航阶段,不过在分析这个阶段之前,我们简要地回顾下导航流程,大致的流程是这样的:
当你点击了Performance上的重新录制按钮之后,浏览器进程会通知网络进程去请求对应的URL资源;一旦网络进程从服务器接收到URL的响应头,便立即判断该响应头中的content-type字段是否属于text/html类型;如果是,那么浏览器进程会让当前的页面执行退出前的清理操作,比如执行JavaScript中的beforunload事件,清理操作执行结束之后就准备显示新页面了,这包括了解析、布局、合成、显示等一系列操作。
因此,在导航阶段,这些任务实际上是在老页面的渲染主线程上执行的。如果你想要了解导航流程的详细细节,我建议你回顾下《04 | 导航流程:从输入URL到页面展示,这中间发生了什么?》这篇文章,在这篇文中我们有介绍导航流程,而导航阶段和导航流程又有着密切的关联。
回顾了导航流程之后,我们接着来分析第一个阶段的任务图形,为了让你更加清晰观察上图中的导航阶段,我将其放大了,最终效果如下图所示:
观察上图,如果你熟悉了导航流程,那么就很容易根据图形分析出这些任务的执行流程了。
具体地讲,当你点击重新加载按钮后,当前的页面会执行上图中的这个任务:
等到所有的数据都接收完成之后,渲染进程会触发另外一个任务,该任务主要执行Finish load过程,该过程表示网络请求已经完成。
好了,导航阶段结束之后,就进入到了解析HTML数据阶段了,这个阶段的主要任务就是通过解析HTML数据、解析CSS数据、执行JavaScript来生成DOM和CSSOM。那么下面我们继续来分析这个阶段的图形,看看它到底是怎么执行的?同样,我也放大了这个阶段的图形,你可以观看下图:
观察上图这个图形,我们可以看出,其中一个主要的过程是HTMLParser,顾名思义,这个过程是用来解析HTML文件,解析的就是上个阶段接收到的HTML数据。
DOM生成之后,ParserHTML过程继续计算样式表,也就是Reculate Style,这就是生成CSSOM的过程,关于Reculate Style过程,你可以参考我们在《05 | 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?》节的内容,到了这里一个完整的ParserHTML任务就执行结束了。
生成了DOM和CSSOM之后,就进入了第三个阶段:生成页面上的位图。通常这需要经历布局(Layout)、分层、绘制、合成等一系列操作,同样,我将第三个阶段的流程也放大了,如下图所示:
结合上图,我们可以发现,在生成完了DOM和CSSOM之后,渲染主线程首先执行了一些DOM事件,诸如readyStateChange、load、pageshow。具体地讲,如果你使用JavaScript监听了这些事件,那么这些监听的函数会被渲染主线程依次调用。
接下来就正式进入显示流程了,大致过程如下所示。
走到了Composite Layers这步,主线程的任务就完成了,接下来主线程会将合成的任务完全教给合成线程来执行,下面是具体的过程,你也可以对照着Composite、Raster和GPU这三个指标来分析,参考下图:
结合渲染流水线和上图,我们再来梳理下最终图像是怎么显示出来的。
通过对Main指标的分析,我们把导航流程,解析流程和最终的显示流程都串起来了,通过Main指标的分析,我们对页面的加载过程执行流程又有了新的认识,虽然实际情况比这个复杂,但是万变不离其宗,所有的流程都是围绕这条线来展开的,也就是说,先经历导航阶段,然后经历HTML解析,最后生成最终的页面。
本文主要的目的是让我们学会如何分析Main指标。通过页面加载过程的分析,就能掌握一套标准的分析Main指标的方法,在该方法中,我将加载过程划分为三个阶段:
在导航流程中,主要是处理响应头的数据,并执行一些老页面退出之前的清理操作。在解析HTML数据阶段,主要是解析HTML数据、解析CSS数据、执行JavaScript来生成DOM和CSSOM。最后在生成最终显示位图的阶段,主要是将生成的DOM和CSSOM合并,这包括了布局(Layout)、分层、绘制、合成等一系列操作。
通过Main指标,我们完整地分析了一个页面从加载到显示的过程,了解这个流程,我们自然就会去分析页面的性能瓶颈,比如你可以通过Main指标来分析JavaScript是否执行时间过久,或者通过Main指标分析代码里面是否存在强制同步布局等操作,分析出来这些原因之后,我们可以有针对性地去优化我们的程序。
在《18| 宏任务和微任务:不是所有任务都是一个待遇》这节中介绍微任务时,我们提到过,在一个任务的执行过程中,会在一些特定的时间点来检查是否有微任务需要执行,我们把这些特定的检查时间点称为检查点。了解了检查点之后,你可以通过Performance的Main指标来分析下面这两段代码:
<body>
<script>
let p = new Promise(function (resolve, reject) {
resolve("成功!");
});
p.then(function (successMessage) {
console.log("p! " + successMessage);
})
let p1 = new Promise(function (resolve, reject) {
resolve("成功!");
});
p1.then(function (successMessage) {
console.log("p1! " + successMessage);
})
</script>
</bod>
<body>
<script>
let p = new Promise(function (resolve, reject) {
resolve("成功!");
});
p.then(function (successMessage) {
console.log("p! " + successMessage);
})
</script>
<script>
let p1 = new Promise(function (resolve, reject) {
resolve("成功!");
});
p1.then(function (successMessage) {
console.log("p1! " + successMessage);
})
</script>
</body>
今天留给你的任务是结合Main指标,来分析上面这两段代码中微任务执行的时间点有何不同,并给出分析结果和原因。欢迎在留言区与我交流。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
评论