本文示例源代码请戳 github博客,建议你们动手敲敲代码。
浏览器渲染页面的过程css
从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:html
本文讨论第五个部分,即浏览器对内容的渲染,这一部分(渲染树构建、布局及绘制),又能够分为下面五个步骤:node
须要明白,这五个步骤并不必定一次性顺序完成。若是 DOM 或 CSSOM 被修改,以上过程须要重复执行,这样才能计算出哪些像素须要在屏幕上进行从新渲染。实际页面中,CSS 与 JavaScript 每每会屡次修改 DOM 和 CSSOM。git
在详细说明以前咱们来看一下浏览器线程。这将有助于咱们理解后续内容。github
浏览器是多线程的,它们在内核制控下相互配合以保持同步。一个浏览器至少实现三个常驻线程:JavaScript 引擎线程,GUI 渲染线程,浏览器事件触发线程。web
浏览器从网络或硬盘中得到HTML字节数据后会通过一个流程将字节解析为DOM树:ajax
<html>、<body>
这样的标签以及标签中的字符串和属性等都会被转化为令牌,每一个令牌具备特殊含义和一组规则)。令牌记录了标签的开始与结束,经过这个特性能够轻松判断一个标签是否为子标签(假设有<html>
与<body>
两个标签,当<html>
标签的令牌还未遇到它的结束令牌</html>
就碰见了<body>
标签令牌,那么<body>
就是<html>
的子标签)。整个DOM树的构建过程其实就是: 字节 -> 字符 -> 令牌 -> 节点对象 -> 对象模型,
下面将经过一个示例HTML代码与配图更形象地解释这个过程。chrome
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> <title>Critical Path</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
当上述HTML代码碰见<link>标签时,浏览器会发送请求得到该标签中标记的CSS文件(使用内联CSS能够省略请求的步骤提升速度,但没有必要为了这点速度而丢失了模块化与可维护性),style.css中的内容以下:segmentfault
body { font-size: 16px } p { font-weight: bold } span { color: red } p span { display: none } img { float: right }
浏览器得到外部CSS文件的数据后,就会像构建DOM树同样开始构建CSSOM树,这个过程没有什么特别的差异。浏览器
在构建了DOM树和CSSOM树以后,浏览器只是拥有了两个互相独立的对象集合,DOM树描述了文档的结构与内容,CSSOM树则描述了对文档应用的样式规则,想要渲染出页面,就须要将DOM树与CSSOM树结合在一块儿,这就是渲染树。
CSS采用了一种叫作盒子模型的思惟模型来表示每一个节点与其余元素之间的距离,盒子模型包括外边距(Margin),内边距(Padding),边框(Border),内容(Content)。页面中的每一个标签其实都是一个个盒子
布局阶段会从渲染树的根节点开始遍历,而后肯定每一个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每一个元素在屏幕内的确切位置与大小,全部相对的测量值也都会被转换为屏幕内的绝对像素值。
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critial Path: Hello world!</title> </head> <body> <div style="width: 50%"> <div style="width: 50%">Hello world!</div> </div> </body> </html>
当Layout布局事件完成后,浏览器会当即发出Paint Setup与Paint事件,开始将渲染树绘制成像素,绘制所需的时间跟CSS样式的复杂度成正比,绘制完成后,用户就能够看到页面的最终呈现效果了。
咱们对一个网页发送请求并得到渲染后的页面可能也就通过了1~2秒,但浏览器其实已经作了上述所讲的很是多的工做,总结一下浏览器关键渲染路径的整个过程:
为了直观的观察浏览器加载和渲染的细节,本地用nodejs搭建一个简单的HTTP Server。
index.js
const http = require('http'); const fs = require('fs'); const hostname = '127.0.0.1'; const port = 8080; http.createServer((req, res) => { if (req.url == '/a.js') { fs.readFile('a.js', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/plain'}); setTimeout(function () { res.write(data); res.end() }, 5000) }) } else if (req.url == '/b.js') { fs.readFile('b.js', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.write(data); res.end() }) } else if (req.url == '/style.css') { fs.readFile('style.css', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/css'}); res.write(data); res.end() }) } else if (req.url == '/index.html') { fs.readFile('index.html', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); res.end() }) } }).listen(port, hostname, () => { console.log('Server running at ' + hostname + ':' + port); });
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>浏览器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> <script src='http://127.0.0.1:8080/a.js'></script> </head> <body> <p id='header'>1111111</p> <script src='http://127.0.0.1:8080/b.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
style.css
#header{ color: red; }
a.js、b.js
暂时为空
能够看到,服务端将对a.js的请求延迟5秒返回。Server启动后,在chrome浏览器中打开http://127.0.0.1:8080/index.html
咱们打开chrome的调试面板
第一次解析html的时候,外部资源好像是一块儿请求的,说资源是预解析加载的,就是说style.css和b.js是a.js形成阻塞的时候才发起的请求,图中也是能够解释得通,由于第一次Parse HTML的时候就遇到阻塞,而后预解析就去发起请求,因此看起来是一块儿请求的。
咱们修改一下html代码
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>浏览器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
由于a.js的延迟,解析到a.js所在的script标签的时候,a.js尚未下载完成,阻塞并中止解析,以前解析的已经绘制显示出来了。当a.js下载完成并执行完以后继续后面的解析。固然,浏览器不是解析一个标签就绘制显示一次,当遇到阻塞或者比较耗时的操做的时候才会先绘制一部分解析好的。
修改index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>浏览器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> </body> </html>
由于a.js的阻塞使得解析中止,a.js下载完成以前,页面没法显示任何东西。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>浏览器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
解析到js文件时出现阻塞。阻塞后面的解析,致使后面的不能很快的显示。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>浏览器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> </body> </html>
解析到a.js部分的时候,页面要显示的东西已经解析完了,a.js不会影响页面的呈现速度。
由上面咱们能够总结一下
下面咱们来看下异步js
接下来咱们对比下 defer 和 async 属性的区别:
其中蓝色线表明JavaScript加载;红色线表明JavaScript执行;绿色线表明 HTML 解析。
<script src="script.js"></script>
没有 defer 或 async,浏览器会当即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,若是已经加载好,就会开始执行——不管此刻是 HTML 解析阶段仍是 DOMContentLoaded 触发以后。须要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发以前或以后执行,但必定在 load 触发以前执行。
defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未中止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成以后(这两件事情的顺序无关),会执行全部由 defer-script 加载的 JavaScript 代码,而后触发 DOMContentLoaded 事件。
defer 与相比普通 script,有两点区别:
服务端将style.css
的相应也设置延迟。
fs.readFile('style.css', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/css'}); setTimeout(function () { res.write(data); res.end() }, 5000) })
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>浏览器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js' async></script> <script src='http://127.0.0.1:8080/b.js' async></script> </body> </html>
能够看出来,css文件不会阻塞HTML解析,可是会阻塞渲染,致使css文件未下载完成以前已经解析好html也没法先显示出来。
咱们把css调整到尾部
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>浏览器渲染</title> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> <script src='http://127.0.0.1:8080/a.js' async></script> <script src='http://127.0.0.1:8080/b.js' async></script> </body> </html>
这是页面能够渲染了,可是没有样式。直到css加载完成
以上咱们能够简单总结。