本文中浏览器特指Chrome浏览器
开始以前说说几个概念,以及在准备写这篇文章以前对浏览器的渲染机制的了解:javascript
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 是更优的。)
查阅了一些关于浏览器渲染机制的文章后,获得如下比较重要或者有争议性的观点:css
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,按照算出来的规则,经过显卡,把内容画到屏幕上。
出处html
浏览器的主要组件为 (1.1):
1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其余显示的各个部分都属于用户界面。
2. 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
3. 呈现引擎 - 负责显示请求的内容。若是请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
4. 网络 - 用于网络调用,好比 HTTP 请求。其接口与平台无关,并为全部平台提供底层实现。
5. 用户界面后端 - 用于绘制基本的窗口小部件,好比组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操做系统的用户界面方法。
6. JavaScript 解释器。用于解析和执行 JavaScript 代码。
7. 数据存储。这是持久层。浏览器须要在硬盘上保存各类数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(可是轻便)的浏览器内数据库。
值得注意的是,和大多数浏览器不一样,Chrome 浏览器的每一个标签页都分别对应一个呈现引擎实例。每一个标签页都是一个独立的进程。主流程
呈现引擎一开始会从网络层获取请求文档的内容,内容的大小通常限制在 8000 个块之内。
而后进行以下所示的基本流程:
呈现引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的 DOM 节点。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于建立另外一个树结构:呈现树。
呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。
呈现树构建完毕以后,进入“布局”处理阶段,也就是为每一个节点分配一个应出如今屏幕上的确切坐标。下一个阶段是绘制 - 呈现引擎会遍历呈现树,由用户界面后端层将每一个节点绘制出来。
须要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它没必要等到整个 HTML 文档解析完毕以后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其他内容的同时,呈现引擎会将部份内容解析并显示出来。html5解析算法
HTML 没法用常规的自上而下或自下而上的解析器进行解析。
缘由在于:
1.语言的宽容本质。
2.浏览器从来对一些常见的无效 HTML 用法采起包容态度。
3.解析过程须要不断地反复。源内容在解析过程当中一般不会改变,可是在 HTML 中,脚本标记若是包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容。
因为不能使用常规的解析技术,浏览器就建立了自定义的解析器来解析 HTMLjava处理脚本和样式表的顺序
脚本
网络的模型是同步的。网页做者但愿解析器遇到 <script> 标记时当即解析并执行脚本。文档的解析将中止,直到脚本执行完毕。若是脚本是外部的,那么解析过程会中止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。做者也能够将脚本标注为“defer”,这样它就不会中止文档解析,而是等到解析结束才执行。HTML5 增长了一个选项,可将脚本标记为异步,以便由其余线程解析和执行。
预解析
WebKit 和 Firefox 都进行了这项优化。在执行脚本时,其余线程会解析文档的其他部分,找出并加载须要经过网络加载的其余资源。经过这种方式,资源能够在并行链接上加载,从而提升整体速度。请注意,预解析器不会修改 DOM 树,而是将这项工做交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
样式表
另外一方面,样式表有着不一样的模型。理论上来讲,应用样式表不会更改 DOM 树,所以彷佛没有必要等待样式表并中止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。若是当时尚未加载和解析样式,脚本就会得到错误的回复,这样显然会产生不少问题。这看上去是一个非典型案例,但事实上很是广泛。Firefox 在样式表加载和解析的过程当中,会禁止全部脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受还没有加载的样式表影响时,它才会禁止该脚本。
呈现树构建
在 DOM 树构建的同时,浏览器还会构建另外一个树结构:呈现树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的做用是让您按照正确的顺序绘制内容。node
出处web
根据以上长篇大论,能够归结为如下几点:算法
文章一:
1.浏览器请求到html结构后,并发请求js,css,图片等资源,并非解析到相应节点才去发送网络请求。文章二:
1.HTML解析为dom树,不是简单的自上而下,而是须要不断地反复,好比解析到脚本标签,脚本修改以前已经解析的dom,这就要往回从新解析一遍
2.HTML 解析一部分就显示一部分(无论样式表是否已经下载完成)
3.<script> 标记会阻塞文档的解析(DOM树的构建)直到脚本执行完,若是脚本是外部的,需等到脚本下载并执行完成才继续往下解析。
4.外部资源是解析过程当中预解析加载的(脚本阻塞了解析,其余线程会解析文档的其他部分,找出并加载),而不是一开始就一块儿请求的(实际上看起来也是并发请求的,由于请求不相互依赖)chrome
为了直观的观察浏览器加载和渲染的细节,本地用nodejs搭建一个简单的HTTP Server。
server.js:shell
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() }, 10000) }) } 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); });
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> <script src='http://127.0.0.1:8080/a.js'></script> <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> <p>222222</p> <p>3333333</p> </body> </html>
能够看到,服务端将对a.js的请求延迟10秒返回。
Server启动后,在chrome浏览器中打开http://127.0.0.1:8080/index.html
看一下TimeLine
能够看到,第一次解析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内容太多的时候,浏览器须要分段接收,解析的时候也要分段解析。还能够看到,请求资源的时机是没法肯定的,但确定不是同时请求的,也不是解析到指定标签的时候才去请求,浏览器会自行判断,若是当前操做比较耗时,就会去加载后面的资源。
修改 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下载完成并执行完以后继续后面的解析。固然,浏览器不是解析一个标签就绘制显示一次,当遇到阻塞或者比较耗时的操做的时候才会先绘制一部分解析好的。
修改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。
服务端将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> <img src="emmet.png"> <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