小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
从输入url到页面展示到底发生了什么? 这是一道经典的高热度前端面试题了。这篇文章打算着重讲从浏览器接收到html返回,到页面呈现出来的一个细节过程。即从下图中的domLoading开始讲。
当浏览器的网络线程接收到html返回后,就会将html交给浏览器中的渲染进程进行从上到下的解析。整个解析的流程如下所示:
虽然我们常说js是单线程的,但是浏览器却是多进程的。如下图所示,可以通过谷歌浏览器的更多工具 -> 任务管理器查看浏览器当前的所有进程。
将chrome中的多进程抽象一下,下面这张图就显示了chrome中主要的进程和它们之间的联系了。
那么每个进程具体负责什么呢?
- Browser进程:浏览器的主进程,主要负责界面显示、用户交互、子进程管理,同时提供存储等功能
- GPU进程:用于3D绘制等。
- Renderer进程:渲染进程,主要负责将html、js、css转换为可与用户交互的网页,排版引擎Blink和V8引擎都运行在这个进程中。可以看见任务管理器中有许多个Tab进程,默认情况下,chrome会为每个Tab页创建一个渲染进程。
- Plugin进程:插件进程,如谷歌翻译插件,flash插件等,为了防止插件崩溃,使用插件进程进行隔离。
- Utility进程:常见的网络进程和音频服务进程就属于这一类,其实utility之前都是放在browser进程中的,最近才独立出来。网络进程负责网络资源的加载,音频服务进程负责麦克风等。
浏览器进程这一块介绍完了,回到html解析,需要提前说明的是,渲染进程是一个渐进式的过程,它不需要等到整个html解析完成后,才进行页面绘制。而是在不断处理网络请求的同时,尽快地将已解析部分的页面呈现出来,提供更好的用户体验。
因此什么是尽快呢?本文的主要目的就是探究以下几个问题:
- 样式文件css加载会阻塞DOM树解析渲染吗?
- 图片文件加载会阻塞DOM树解析渲染吗?
- 脚本文件js加载会阻塞DOM树解析渲染吗?
为了更好地进行说明,以一个实际的html为例(本例中请求的文件都是通过本地的node express服务提供的文件,每次请求设置了延时5s返回):
<!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>
<script>
setTimeout(()=>{
console.log(document.querySelectorAll('h1'))
})
</script>
<link rel="stylesheet" href="http://localhost:3000/css/style.css">
</head>
<body>
<h1>样式文件加载会阻塞DOM树解析渲染吗?</h1>
<img src="http://localhost:3000/img/1.jpg">
<p>图片加载会阻塞DOM树解析渲染吗?</p>
<script src="http://localhost:3000/js/1.js"></script>
<p>JS文件加载会阻塞DOM树解析渲染吗?</p>
<script defer src="http://localhost:3000/js/2.js"></script>
<script async src="http://localhost:3000/js/3.js"></script>
<p>带有defer或者async属性的JS文件加载会阻塞DOM树解析渲染吗?</p>
</body>
</html>
CSS加载不阻塞页面解析,但阻塞页面渲染
为了单独验证CSS文件加载情况,将html中的其他文件请求注释掉,只保留css文件请求
可以看到当css文件还在加载时,已经可以使用document.querySelectorAll了,说明此时dom树已经构建完成。但是页面仍然处于白屏的状态,说明css文件的加载阻塞了页面渲染。
js加载阻塞页面解析和渲染
为了单独验证js文件加载情况,将html中的其他文件请求注释掉,只保留不带async和defer属性的js文件请求
<!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>
<script>
setTimeout(()=>{
console.log(document.querySelectorAll('p'))
})
</script>
<!-- <link rel="stylesheet" href="http://localhost:3000/css/style.css"> -->
</head>
<body>
<h1>样式文件加载会阻塞DOM树解析渲染吗?</h1>
<!-- <img src="http://localhost:3000/img/1.jpg"> -->
<p>图片加载会阻塞DOM树解析渲染吗?</p>
<script src="http://localhost:3000/js/1.js"></script>
<p>JS文件加载会阻塞DOM树解析渲染吗?</p>
<script>
setTimeout(()=>{
console.log(document.querySelectorAll('p'))
})
</script>
<!-- <script defer src="http://localhost:3000/js/2.js"></script>
<script async src="http://localhost:3000/js/3.js"></script>
<p>带有defer或者async属性的JS文件加载会阻塞DOM树解析渲染吗?</p> -->
</body>
</html>
可以看到当js文件还在加载时,控制台中先后输出document.querySelecorAll('p')的情况不一样,在完成js加载之前,只有一个p元素,加载完成之后,出现了两个p元素,并且js加载会阻塞DOM树的解析渲染吗? 也显示在了页面上,因此它会阻塞当前script标签后面的内容进行解析和渲染。
然后再看如果带上defer或者async属性后的情况
可以看到当加载完成前,页面上直接出现了两行p,控制台中先后输出的
document.querySelecorAll('p')也是得到的两个p元素。因此加上async或defer属性后不会阻塞DOM树的解析渲染。
图片文件加载不阻塞页面解析和渲染
为了单独验证图片文件加载情况,将html中的其他文件请求注释掉,只保留图片文件请求
可以看到当图片文件还在加载的时候,文字已经渲染到页面上了,等到图片加载完成,页面会进行重排和重绘。
最后,再来考虑一个问题
如果页面有十个js请求,每个请求都延时5s返回,总共需要多少时间完成请求?
<h1>样式文件加载会阻塞DOM树解析渲染吗?</h1>
<!-- <img src="http://localhost:3000/img/1.jpg"> -->
<p>图片加载会阻塞DOM树解析渲染吗?</p>
<script src="http://localhost:3000/js/1.js"></script>
<script src="http://localhost:3000/js/2.js"></script>
<script src="http://localhost:3000/js/3.js"></script>
<script src="http://localhost:3000/js/4.js"></script>
<script src="http://localhost:3000/js/5.js"></script>
<script src="http://localhost:3000/js/6.js"></script>
<script src="http://localhost:3000/js/7.js"></script>
<script src="http://localhost:3000/js/8.js"></script>
<script src="http://localhost:3000/js/9.js"></script>
<script src="http://localhost:3000/js/10.js"></script>
<p>JS文件加载会阻塞DOM树解析渲染吗?</p>
可以看到,过了5s后,有6个js文件请求返回了,再过了5s后,其余4个js文件请求返回。总共花了10s完成所有文件的请求。chrome对于同域名最大并发请求的数目就为6,因此每次最多处理6个并发请求。上面只以js文件举例,其他的如css和图片文件也是一样的道理。至于为什么设置为6的一个具体分析,可以参考浏览器同域名请求的最大并发数限制。
参考: