一、写在前面
我发现即便工作很久了,我还是不会用performance性能分析工具,可能很多小伙伴跟我有一样的问题!
于是我怒学了一通performance性能工具的使用,想写一篇如何使用performance工具的教程的,但是我发现字节团队的这篇文章已经写的很好了。
掘金不需要多一篇一模一样的文章,所以我本来打算放弃写类似的文章的,但是最近工作中正好涉及到网页性能相关的知识,我发现网上的文章说法的各不相同,就比如这个问题:
css和js到底阻不阻塞DOM?如何阻塞的?细节是什么?原因是什么?
如果你对这个问题感觉比较模糊,能说个大概,但是不清晰!那么接下来就是你看这篇文章的意义!
本篇文章尝试从performance性能分析工具的角度来探讨一个网页从url到渲染完毕发生的所有事情,并且信仰口说无凭,要么拿出实验证据,要么有performance的铁证,因为网上的信息太多了,众说纷纭,我们只能自己尽可能找到最贴近真相的答案。
本篇文章会从performance性能分析工具的角度带着大家分析一个网站的解析,即便没有任何performance基础都可以,我会在文中会提到相关的知识!
二、再谈渲染进程
chrome浏览器是多进程架构,也就是说每一个新开的tab都是一个独立的进程,然而除了浏览器的每一个tab的渲染进程之外,还有浏览器进程、GPU进程、插件进程、网络进程等和浏览器进程共同协作!
我们可以通过chrome浏览器的任务管理器查看目前浏览器的进程占用内存和CPU的情况:
查看方式是:右上角菜单 -> 更多工具 -> 任务管理器
而我们最需要了解的就是渲染进程了,也就是浏览器的内核,它负责将html、css、js等静态的资源变成可交互的用户界面。
渲染进程由GUI线程、js引擎线程、webWorker线程、合成线程、光栅线程组成、预解析线程等组成。GUI线程负责解析HTML以及CSS、计算样式等,js引擎负责解析和执行js脚本,webWorker线程负责解析和执行webWorker中的js程序,合成线程负责合成图层,以便提升GPU绘制的性能,光栅线程负责将renderTree信息转化为像素信息,预解析线程负责提前扫描一下提前下载资源。
三、上手performance
接下来我们就用performance来记录掘金主页的渲染过程,并且详细分析其存在的各种知识点,为了方便理解,我把devTools调试为中文的翻译。
在上手之前,你也可以阅读这篇文章,以便更好的理解下面的内容。
通过性能分析工具,我得到了一张这样的performance性能分析工具图,这里要说明一下,由于网络和硬件的不同,读者朋友们的图可以和我的有很大不同。
performance在录制的过程中会从各个进程里读取数据,汇总到一起生成了这样的一份动态的报告!
可以看到在宏观上,网络进程、渲染进程、GPU进程是同时在工作的,互不影响,这更加佐证了浏览器多进程架构的特点。
接下来我们分析一下如何从一个url变成页面的,故事还是得从网络说起!
网络
当我们的时间来到第一刻的时候,便是请求html资源,当我们向服务器请求这个资源的时候,会经历很多过程,例如DNS解析、建立连接、可能会发生重定向等。
当我们在看performance性能分析工具的时候,如果没有那么多时间全部都操作一遍,掌握两个地方就可以了,一个是网络,一个火焰图。
上面是我截取的一部分网络的过程,可以看到那个蓝色的就是juejin首页的html资源的加载的过程,整个过程由网络IO进程负责。一般来说,有以下的规律:
颜色 | 含义 |
---|---|
蓝色 | html资源 |
紫色 | css资源 |
橙黄色 | js资源 |
绿色 | 图片资源 |
灰色 | json资源 |
并且每一个资源的加载过程都有这样的图表示:
他们分别?️左侧线条、浅色区别、深色区域、右侧线条组成,读者朋友可以再翻上去看一下是不是这样的感觉,或者在自己的performance中感受一下!
他们代表的含义就是:
图形 | 含义 |
---|---|
左侧线条 | 网络进程队列、DNS、建立连接的耗时 |
浅色区域 | 等待服务器响应的耗时 |
深色区域 | 下载资源的耗时 |
右侧线条 | 等待渲染进程接管的耗时 |
我们也可以在网络中看到每一个资源加载的详细过程:
可以看到其实首页加载主要的耗时还是在等待服务器响应这一块,这是网络方面的瓶颈!
预解析
在下载首页html资源的时候,其实并不是下载完了渲染进程才开始做事情,其实渲染进程从开始接受第一个html字节流的时候就开始准备要做事情了!
这个事情就是预解析,预解析是浏览器为了提升网页渲染性能而设计的,我们可以设想一下,因为js的加载和执行都是阻塞DOM的解析和渲染的,因此如果一定要等到html资源加载完毕,才开始下载js以及其他资源的话,那么IO的时间消耗就会更加的明显,于是浏览器在渲染进程里设计了一个小线程,专门在解析html之前,就预解析一遍html,将需要下载的资源并行的加载,这样的话就提前规避了很多阻塞的问题!以提升网站性能!但是预解析是从什么时候开始的呢!
还是用performance性能分析工具来分析一下吧!
我们可以看到css、js资源是在深色部分就开始了请求,那么我们可以得出一个结论,预解析的过程并不会等待所有html资源都下载完毕放在内存才开始工作,而是从接受html资源第一个字节开始就准备进行预解析的工作了!
实际上正确的过程是当html资源接受第一个字节开始,网络进程就开始和浏览器进程通信,他们的对话就像下面这个样子!
网络进程:嘿!浏览器老哥,我这边开始进货了,你那边要不要通知一下渲染进程啊!
浏览器进程:好的,老弟,我马上给渲染进程打电话哈!说罢:浏览器给渲染进程打电话….
渲染进程:怎么了,浏览器大哥,有啥事么!
浏览器进程:那啥,网络那边开始进货了,你这边啥时候准备验货啊。
渲染进程:可以呀!我时刻准备着呢,不过我就不亲自动手了,我派个预解析线程先简单验一下。你直接叫网络那边给我一箱、一箱送货就行。
浏览器进程:好的,没问题!
于是网络进程那边不断给渲染进程送上html资源,而预解析线程就开始扫描所有可能发送请求的标签,比如没有添加perload、perfetch属性的link标签、没有async和defer的script标签、带src的img标签等等!
结论:预解析的过程并不是在html资源下载完了之后才进行,而是在下载过程中就开始了。
js、css阻塞DOM解析么?
这个问题实际上应该拆解成:js的下载和执行,以及css的下载和解析分别影响DOM的解析么?
我们分别来看:
js:
我们准备这样一个html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>我是script之前的h1</h1>
<script src="http://localhost:5501/block.js"></script>
<h1>我是script之后的h1</h1>
</body>
</html>
// block.js
const now = () => performance.now();
const time = now();
while (now() - time <= 2000) {}
如果我们做上面的这样的实验,就可以看到一个现象!(注意把网络调成低速3G会更加明显),首先界面上出现了“script之前的h1”,等了4s之后绘制出了“script之后的h1”,我们简单分析一下这个现象:
其中2s是block.js脚本执行的时间,因为我们的脚本就是这个效果,那么另外的2s中就是在低速3G的情况下block.js这个文件的网络耗时,说明了一个问题,js对于DOM的阻塞效果至少在用户来看,是js的下载、解析、执行都会阻塞DOM的渲染。
所以我们通过这个简单的实验,可以得出至少第一个结论:js的下载和js的执行都会阻塞DOM的解析和渲染。
但是事实真的是这样子的么?当我们掌握了performance性能分析工具之后,我们可以进一步看看到底是不是这样的!我们判断的核心依据应该是在资源加载的这个过程中,解析HTML和绘制有没有进行,如果有进行,那么就js是不阻碍DOM的解析和渲染的。
我们可以看到在下载js文件资源之前,渲染进程都会先进行一次解析和渲染,这是为了把script标签之前的内容先渲染出来,以便获得更好的用户体验,
可以看到js在下载资源的过程中,是不会出现解析DOM和渲染的工作的,所以再一次印证了上面提到的观点。
css:
css一般情况下我们指的是使用link标签外联的css样式,因为内联的css不存在下载这个过程,所以不用谈论下载对DOM解析造成的影响,我们要研究的问题是css外部资源在加载和解析过程中是否会阻塞DOM的渲染。
准备下面这样的样例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link
rel="stylesheet"
href="https://necolas.github.io/normalize.css/latest/normalize.css"
/>
</head>
<body>
<h1>我是link标签之后的DOM结构</h1>
</body>
</html>
效果如下:
通过实验现象,我们可以看到过了几秒后,界面上才出现了DOM,因此通过这个现象我们就可以得出结论,css的加载过程阻塞DOM的渲染,也就是绘制!
但是阻塞解析么?要知道,渲染和解析是两个概念,解析是指将静态的元素标签解析为内存中的一种数据结构,并维护好子父级的关系。而渲染是指将解析后的DOM结构绘制为界面上可见的视图。
这个时候我们依然借助performance工具来看一下是否阻塞解析:
从performance性能分析工具的体现来看,在css还在网络过程中,依然存在DOM解析的任务进行,因此可以看出css的网络过程不会阻塞DOM解析,但是会阻塞渲染,因为一般来说解析和渲染是先后会出现的,大家经常做performance分析的话就会发现这一现象,但是在第一次解析DOM的过程中并未存在渲染的任务,相反在css下载结束之后,这个时候解析DOM后才出现渲染,更加证明了css的网络过程会阻塞DOM的渲染,下面是performance证明:
因此我们可以通过以上的实验得出以下结论:css的网络过程阻塞DOM的绘制,但不阻塞DOM的解析。
宏任务、微任务
我们都知道在js当中存在宏任务和微任务的概念,可能在之前我们都是通过看文章来理解这个概念,但是实际上在performance中,我们可以以肉眼可见的方式去观测宏任务和微任务的执行过程!
我们先来回顾一下,创建宏任务和微任务的方式有哪些!
宏任务 | 微任务 |
---|---|
setTimeout | Promise |
setInterval | MutationObserver |
MessageChannel | nextTick(node) |
setImmediate | process(node) |
I/O | process(node) |
其实不仅仅有上面这些,创建宏任务的方式还有 script标签、事件回调、DOM解析、系统内程序
下面我们通过一个案例并做一次性能分析来进行详细的说明:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./first.js"></script>
<script src="./second.js"></script>
<button onclick="macro()">创建宏任务</button>
</body>
</html>
//first.js
const microTask = (from) => {
console.log(`我是由${from}一个微任务`);
};
const macroTask = (from) => {
console.log(`一个由${from}创建的宏任务`);
};
const macro = () => {
console.log("一个click事件创建的宏任务");
};
macroTask("script");
Promise.resolve().then(() => {
microTask("promise");
});
setTimeout(() => {
macroTask("setTimeout");
}, 10);
//seond.js
const messagechannel = new MessageChannel();
const port = messagechannel.port1;
messagechannel.port2.onmessage = () => macroTask("messagechannel");
port.postMessage(null);
通过以上的可以演示大部分宏任务的创建方式,他们的performance是这样的:
其中每一个灰色的任务都是一个或多个独立的宏任务,他们是紧密相连的,这个就是事件循环,灰色下面的第二层每一个独立的块都是一个独立的宏任务,宏任务可以是黄颜色的代表js任务、蓝色则代表解析html的任务、绿色代表绘制也是一个宏任务。在一个宏任务下面如果有微任务,那么会放在末尾执行,如下图所示:
所以如果今后当我们看到某一个任务比较长的时候,performance也会标红,这个就是我们需要优化的空间,因为长时间的纯js的宏任务会阻塞DOM的渲染。
时间线
实际上在performance性能分析工具上有这么几个时间点非常重要,他们分别是:
简称 | 全称 | 含义 |
---|---|---|
FP | First paint | 页面在导航后首次呈现出不同于导航前内容的时间点 |
FCP | First Contentful Paint | 首次绘制任何文本,图像,非空白canvas或SVG的时间点。 |
DCL | DOMContentLoaded | HTML加载完成的时间点 |
L | onLoad | 页面所有资源加载完成的时间点 |
LCP | Largest Contentful Paint | 可视区域“内容”最大的可见元素开始出现在页面上的时间点。 |
FMP | First Meaningful Paint | 首次绘制页面“主要内容”的时间点。 |
下图展示了从浏览器卸载旧页面到新页面加载完成的整个过程,每个页面事件穿插在每个页面加载阶段之间的过程:
下面我们来看一下掘金的这个几个事件节点所处的位置:
-
第615毫秒时,开始绘制第一个像素点,紧接着第一个文本开始绘制,FP和FCP是在一起的,并且FP一定永远都在最前面。
-
第1231毫秒时,HTML加载完成
-
第3072毫秒时,所有资源加载完毕
-
第4124毫秒时,页面“主要内容”绘制完成
三、参考资料
Chrome DevTools Performance 功能详解
快速掌握 Performance 性能分析:一个真实的优化案例
现代浏览器架构
inside-browser-part1
原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的
四、最后的话
以下是我的其他文章,欢迎阅读
shell、bash、zsh、powershell、gitbash、cmd这些到底都是啥?
从0到1开发一个浏览器插件(通俗易懂)
用零碎时间个人建站(200+赞)
另外我有一个自己的网站,欢迎来看看 new-story.cn
创作不易,如果您觉得文章有任何帮助到您的地方,或者触碰到了自己的知识盲区,请帮我点赞收藏一下,或者关注我,我会产出更多高质量文章,最后感谢您的阅读,祝愿大家越来越好。