本文中浏览器特指Chrome浏览器
开始之前说说几个概念,以及在准备写这篇文章之前对浏览器的渲染机制的了解:
DOM:Document Object Model,浏览器将HTML解析成树形的数据结构,简称DOM。
CSSOM:CSS Object Model,浏览器将CSS代码解析成树形的数据结构
Render Tree:DOM 和 CSSOM 合并后生成 Render Tree(Render Tree 和DOM一样,以多叉树的形式保存了每个节点的css属性、节点本身属性、以及节点的孩子节点,display:none 的节点不会被加入 Render Tree,而 visibility: hidden 则会,所以,如果某个节点最开始是不显示的,设为 display:none 是更优的。)
查阅了一些关于浏览器渲染机制的文章后,得到以下比较重要或者有争议性的观点:
1.Create/Update DOM And request css/image/js:浏览器请求到HTML代码后,在生成DOM的最开始阶段(应该是 Bytes → characters 后),并行发起css、图片、js的请求,无论他们是否在HEAD里。注意:发起 js 文件的下载 request 并不需要 DOM 处理到那个 script 节点,比如:简单的正则匹配就能做到这一点,虽然实际上并不一定是通过正则:)。这是很多人在理解渲染机制的时候存在的误区。
2.Create/Update Render CSSOM:CSS文件下载完成,开始构建CSSOM
3.Create/Update Render Tree:所有CSS文件下载完成,CSSOM构建结束后,和 DOM 一起生成 Render Tree。
4.Layout:有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为Layout,顾名思义就是计算出每个节点在屏幕中的位置。
5.Painting:Layout后,浏览器已经知道了哪些节点要显示(which nodes are visible)、每个节点的CSS属性是什么(their computed styles)、每个节点在屏幕中的位置是哪里(geometry)。就进入了最后一步:Painting,按照算出来的规则,通过显卡,把内容画到屏幕上。
浏览器的主要组件为 (1.1):
1.用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
2.浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
3.呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
4.网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
5.用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
6.JavaScript 解释器。用于解析和执行 JavaScript 代码。
7.数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
值得注意的是,和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程。
主流程
呈现引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内。
然后进行如下所示的基本流程:
可以看到,第一次解析html的时候,外部资源好像是一起请求的,最后一次Finish Loading是a.js的,因为服务端延迟的10秒钟。文章二中说资源是预解析加载的,就是说style.css和b.js是a.js造成阻塞的时候才发起的请求,图中也是可以解释得通,因为第一次Parse HTML的时候就遇到阻塞,然后预解析就去发起请求,所以看起来是一起请求的。
将index.html内容增加足够多,并且在最后面才加入script:<!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='hh'>1111111</p> <p>重复</p> <p>重复</p> .... ....重复5000行 <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> <p>3333333</p> </body> </html>
多刷新几次,查看TimeLine
可以发现,当html内容太多的时候,浏览器需要分段接收,解析的时候也要分段解析。还可以看到,请求资源的时机是无法确定的,但肯定不是同时请求的,也不是解析到指定标签的时候才去请求,浏览器会自行判断,如果当前操作比较耗时,就会去加载后面的资源。
HTML 是否解析一部分就显示一部分
修改 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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> <p>3333333</p> </body> </html>
因为a.js的延迟,解析到a.js所在的script标签的时候,a.js还没有下载完成,阻塞并停止解析,之前解析的已经绘制显示出来了。当a.js下载完成并执行完之后继续后面的解析。当然,浏览器不是解析一个标签就绘制显示一次,当遇到阻塞或者比较耗时的操作的时候才会先绘制一部分解析好的。
<script>标签的位置对HTML解析有什么影响
修改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/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> </body> </html>
还是因为a.js的阻塞使得解析停止,a.js下载完成之前,页面无法显示任何东西。
整个处理过程中,Parse HTML 3次,计算元素样式1次,页面布局计算1次,绘制一次。
修改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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> </body> </html>
解析到a.js部分的时候,页面要显示的东西已经解析完了,a.js不会影响页面的呈现速度。
整个处理过程中,Parse HTML 3次,计算元素样式2次,页面布局计算1次,绘制一次。
修改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"> </head> <body> <p id='hh'>1111111</p> <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
阻塞后面的解析,导致不能很快的显示。
整个处理过程中,Parse HTML 3次,计算元素样式2次,页面布局计算2次,绘制2次。
可以发现浏览器优化得非常好,当阻塞在a.js的时候,现将已经解析的部分显示(计算元素样式,布局排版,绘制),当a.js下载好后接着解析和显示后面的(因为a.js后面还有要显示到页面上的元素,所以还需要进行1次计算元素样式,布局排版,绘制)修改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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <script src='http://127.0.0.1:8080/a.js'></script> <p>3333333</p> <script> document.getElementById("hh").style.height="200px"; </script> </body> </html>
a.js阻塞的时候,排版,绘制1次;a.js下载完后重排,重绘一次;修改DOM,引起重排,重绘一次。是不是这样呢?看下图
事实是修改DOM并没有引起重排,重绘。因为浏览器将a.js下载完成并执行后的一次重排和重绘与修改DOM本应该导致的重排和重绘积攒一批,然后做一次重排,重绘
浏览器是聪明的,它不会你每改一次样式,它就reflow或repaint一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行reflow。
css文件的影响
服务端将style.css的相应也设置延迟。
修改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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js'></script> </body> </html>
可以看出来,css文件不会阻塞HTML解析,但是会阻塞渲染,导致css文件未下载完成之前已经解析好html也无法先显示出来。
接着修改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> </head> <body> <p id='hh'>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'></script> </body> </html>
同样阻塞渲染
修改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" media="print"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js'></script> </body> </html>
注意media="print"
因为指定了media="print",样式不起作用,不会阻塞渲染。
<link href="style.css" rel="stylesheet">
<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">
第一条声明阻塞渲染,匹配所有情况。
第二条声明一样阻塞渲染:"all" 是默认类型,如果你未指定任何类型,则默认为 "all"。因此,第一条声明和第二条声明实际上是一样的。
第三条声明有一条动态媒体查询,在页面加载时判断。根据页面加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞。
最后一条声明只适用打印,因此,页面在浏览器中首次加载时,不会阻塞渲染。但是。。。看一下火狐的表现
图片资源的影响
修改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" media="print"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> [站外图片上传中……(2)] <p>3333333</p> </body> </html>
图片比较大,2M多,但服务端还是要延迟10秒响应。
图片既不阻塞解析,也不阻塞渲染。
图片未请求回来之前,先进行一次layout和paint,paint的范围就是页面初始的可视区域。当返回一部分图片信息后(估计是得到了图片的尺寸),再进行一次layout和paint,paint的范围受到图片尺寸的影响。当图片信息全部返回时,最后进行一次paint。
如果固定img的宽高,当返回一部分图片信息后,不会再layout,但仍会paint一次。
补充:图片用作背景(不是写在CSS文件内)是在Recalculate Style的时候才发起的请求,layout、paint次数和固定宽高的img一样。背景图属性写在CSS文件里,则CSS文件下载并执行Recalculate Style的时候才会请求图片。参考
浏览器的渲染原理简介
浏览器的工作原理:新式网络浏览器幕后揭秘
JS 一定要放在 Body 的最底部么?聊聊浏览器的渲染机制
https://blog.chromium.org/2015/03/new-javascript-techniques-for-rapid.html
https://developers.google.cn/web/fundamentals/performance/critical-rendering-path/render-blocking-css
网友评论