浏览器的渲染过程
前言
最近在面试的时候遇到了一个前端的优化问题,答得不好,回来之后,就想系统的学习一下,发现想掌握好性能优化,就必须了解好浏览器的整个渲染过程。所以需要先学习渲染过程,却发现网上的相关资料少的可怜,能找到的只有一张很常见的图和几句话:http解析器解析生成DOM树,css解析器解析css生成CSSOM,DOM树和CSSOM树结合生成Render Tree之类的话,完全get不到重点。在经过自己几天的努力之后,大致了解了其渲染过程。接下来,让我们一起来深入了解其渲染过程。
渲染过程
我们在浏览器输入url请求之后,会向DNS服务器请求IP地址、进行TCP三次握手等操作,这些不是本文的重点,我们不进行研究,我们从服务拿到字符串(代码)后,开始说起。首先,我们先来一段简单的代码(如下所示),然后我们在浏览器中打开,并运行这个文件,观看渲染过程的日志。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
从上面的日志中可以看出,如果html文件中没有css、js代码的时候,只会调用html解析器进行解析,生成DOM文档而已,那么如果文件有css文件呢?接下来,我们在demo1.html中添加style标签为h1添加样式。代码如下,然后运行代码,看看其日志是怎么样的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
/* 新增加的样式 */
h1 {
color : red;
}
</style>
<body>
<h1>Hello World</h1>
</body>
</html>
我们发现,虽然添加了css样式,样式也生效了,但是并没有启动css解析器,这不科学啊,说好得css解析器解析css生成CSSOM树呢?接下来我们试试将css样式表移出来,放在style.css文件里面。
h1 {
color : red;
}
在demo1.html中使用link标签引入css样式表。然后执行一下,看看其打印的日志。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<!-- 引入样式表 -->
<link href = "./style.css" rel = "stylesheet">
<body>
<h1>Hello World</h1>
</body>
</html>
从日志中可以看出,终于调用了css解析器了,这是因为有了外部资源style.css文件,在html解析的过程中,遇到要请求外部资源的时候,发送了一个send请求,然后接收资源,但是我们发现,在请求和接收资源的过程中,耗时都是0,这说明了该渲染线程并没有在做这些事,而是新开了一个线程,专门在处理这些请求。完成会将其获得的资源放在一个任务队列里面,待html解析器完成工作之后,就会开始去任务队列里面找任务(这里指style.css),然后调用css解析器解析生成CSSOM。所以说不是所有的css代码都是由css解析器解析的,只有外部文件才是。
这里就涉及到了一个性能调优的点:减少请求的次数和文件大小,(为什么会涉及到呢,你或许有疑惑)
首先,我们发现,在加载外部资源的时候,如果有多个资源需要请求,而这个时候,html解析器已经完成了工作,css解析器完成了任务队列里面的任务,任务队列里却没有任务了,但是还有请求没有完成,这个时候就需要等待了,所以减少请求的次数和文件大小可以优化性能。
讲完了css,接下来我们讲讲js,由于js可以操作doucment文档,所以我们不能在DOM树完成之后再去调用javascript引擎执行js文件。所以在遇到js代码的时候,渲染的主线程只能停止html解析,调用js引擎来执行js代码,上代码来看看实际情况。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script>
var tag = document.getElementById("title");
console.log(tag);
</script>
<body>
<h1 id = "title">Heelo World</h1>
</body>
<script>
var tag2 = document.getElementById("title");
console.log(tag2);
</script>
</html>
可以看出,先执行了第一个script代码,在获取id为title的h1标签的时候,由于还没有解析到,所以是找不到h1的,在控制台打印出 “ null”,在执行第二个script代码的时候,h1已经在DOM树里面了,这个时候就可以获取到h1。
将js代码移出html页面,使用外部加载资源看看。
// 外部的gettag.js文件
var tag = document.getElementById("title");
console.log(tag);
var tag2 = document.getElementById("title");
console.log(tag2);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<!-- 外部加载js -->
<script src="./gettag.js"></script>
<body>
<h1 id = "title">Hello World</h1>
</body>
</html>
我们发现,两个console.log打印都为null,可以得出一个结论,虽然js是外部引用资源,但是因为是同步任务,所以我们的主线程会阻塞,等到js的资源拿到并执行完成之后,才继续解析html。这里有两个解决方法,第一:在script放在最后,第二,为script 添加async。这里示范一下第二种方法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<!-- 外部加载js -->
<script src="./gettag.js" async></script>
<body>
<h1 id = "title">Hello World</h1>
</body>
</html>
可以看到,js不阻塞html解析了。
总结
渲染过程:
- 使用url请求拿到字符串(代码)之后,使用http解析器解析代码生成DOM Tree
- http解析器解析过程中,遇到需要请求外部资源的时候,会重新开启一个线程去请求这些数据,并将结果放入任务队列。
- http解析器解析过程中,遇到 js 代码的时候,会调用javascript引擎执行代码,html停止解析,等到javascript引擎完成工作之后,继续调用html解析器工作。
- http解析器解析过程中,遇到 script 需要外部加载资源,而且是同步的时候, javascript引擎会等待资源加载完成并执行完之后,才会把主线程的执行权交还给html解析器。
- http解析器解析过程中,遇到 style 标签,不会调用css解析器,只有在解析外部样式表的时候才会调用css解析器。
后话
需要这篇文章对你有所帮助,如果其中有什么写错的地方,欢迎指点出来。