浏览器渲染页面原理,reflow、repaint及其优化

浏览器的主要组件包括:

1.      用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的你请求的页面外,其余显示的各个部分都属于用户界面。javascript

2.      浏览器引擎 - 在用户界面和渲染引擎之间传送指令。css

3.      渲染引擎 - 负责显示请求的内容。若是请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。html

4.      网络 - 用于网络调用,好比 HTTP 请求。其接口与平台无关,并为全部平台提供底层实现。java

5.      用户界面后端 - 用于绘制基本的窗口小部件,好比组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操做系统的用户界面方法。node

6.      JavaScript 解释器。用于解析和执行 JavaScript 代码,好比chrome的JavaScript解释器是V8。web

7.      数据存储。这是持久层。浏览器须要在硬盘上保存各类数据,例如 Cookie。新的 HTML 规范 (HTML5)定义了“网络数据库”,这是一个完整(可是轻便)的浏览器内数据库。chrome

 

关键路径渲染(Critical Rendering Path):渐进式数据库

 

浏览器拿到HTML以后的渲染过程:(不一样内核实现不同但大概是这样)express

1.      解析HTML,构建DOM tree。segmentfault

2.      解析CSS,构建CSSOM tree。

3.      合并DOM tree和CSSOM tree,生成render tree。

4.      布局(layout/reflow),计算各元素尺寸、位置。

5.      绘制(paint/repaint),绘制页面像素信息。

6.      浏览器将各层的信息发送给GPU,GPU将各层合成,显示在屏幕上。

当修改了DOM或CSSOM,上述过程当中的一些步骤就会重复执行。

 

构建OM:要通过Bytes  characters  tokens  nodes  object model这个过程。

 

TIPS:

解析HTML遇到外部CSS当即请求 ----CSS文件合并,减小HTTP请求;

新的CSS style修改CSSOM,会从新渲染页面 ----CSS文件应放在头部,缩短首次渲染时间

遇到<img>会发出请求,但不会阻塞,服务器返回图片文件,因为图片占用了必定面积,影响了后面段落的排布,所以浏览器须要回过头来从新渲染这部分代码;(最好图片都设置尺寸,避免从新渲染)

遇到<script> 标签,会当即执行js代码,阻塞渲染。(script最好放置页面最下面)

js修改DOM会从新渲染。 (页面初始化样式不要使用js控制) 

 

reflow回流:

当某个部分发生了变化影响了布局,须要倒回去从新渲染, 该过程称为reflow(回流)。reflow 几乎是没法避免的。如今界面上流行的一些效果,好比树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引发浏览器的 reflow。鼠标滑过、点击……只要这些行为引发了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引发它内部、周围甚至整个页面的从新渲染。一般咱们没法预估浏览器到底会 reflow 哪一部分的代码,它们彼此相互影响。

repaint重绘:

若是只是改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性,将只会引发浏览器 repaint(重绘)。repaint 的速度明显快于 reflow(在IE下须要换一下说法,reflow 要比 repaint 更缓慢)。

reflow必定引发repaint,而repaint不必定要reflow。reflow的成本比repaint高不少,DOM tree里每一个结点的reflow极可能触发其子结点、祖先结点、兄弟结点的reflow。reflow(回流)是致使DOM脚本执行低效的关键因素之一。

 现代浏览器会对回流作优化,它会等到足够数量的变化发生,再作一次批处理回流。

 GoogleChromeLabs里面有一个csstriggers,列出了各个CSS属性对浏览器执行Layout、Paint、Composite的影响。

 

在哪些状况下会致使reflow发生:

l  改变窗囗大小

l  改变文字大小

l  添加/删除样式表

l  内容的改变,如用户在输入框中敲字

l  激活伪类,如:hover (IE里是一个兄弟结点的伪类被激活)

l  操做class属性

l  脚本操做DOM

l  计算offsetWidth和offsetHeight

l  设置style属性

 

优化,尽可能避免reflow:

l  尽量限制reflow的影响范围,修改DOM层级较低的结点。不要经过父级元素影响子元素样式。最好直接加在子元素上。改变子元素样式尽量不要影响父元素和兄弟元素的尺寸。

l  不要一条一条的修改DOM的style,最好经过设置class的方式。 避免触发屡次reflow和repaint。

l  常常reflow的元素,好比动画,position设为fixed或absolute,使其脱离文档流,不影响其它元素的布局。

l  权衡速度的平滑。好比实现一个动画,以1个像素为单位移动这样最平滑,但reflow就会过于频繁,CPU很快就会被彻底占用。若是以3个像素为单位移动就会好不少。

l  不要用tables布局。tables中某个元素一旦触发reflow就会致使table里全部的其它元素reflow。在适合用table的场合,能够设置table-layout为auto或fixed,这样可让table一行一行的渲染,这种作法也是为了限制reflow的影响范围。

l  避免使用css expression(每次都会从新计算)。

l  减小没必要要的 DOM 层级(DOM depth)。改变 DOM 树中的一级会致使全部层级的改变,上至根部,下至被改变节点的子节点。这致使大量时间耗费在执行 reflow 上面。

l  避免没必要要的复杂的 CSS 选择器,尤为是后代选择器(descendant selectors),由于为了匹配选择器将耗费更多的 CPU。

l  尽可能不要频繁的增长、修改、删除元素,能够先把DOM节点抽离到内存中进行复杂的操做而后再display到页面上。(display:none的节点不会被加入render tree,而visibility:hidden会;display:none会触发reflow,而visibility:hidden只会触发repaint,由于layout没有变化)。

 

让要进行复杂操做的元素进行“离线处理”,处理完后一块儿更新:

1.          使用DocumentFragment, DocumentFragment节点不属于文档树,继承的parentNode属性老是null。

 

 
//不建议的作法
 
for(var i = 0 ; i < 10000; i ++) {
 
var p = document.createElement("p");
 
var oTxt = document.createTextNode("段落" + i);
 
p.appendChild(oTxt);
 
document.body.appendChild(p);
 
}
 
//将这些元素添加到DocumentFragment中,再将DocumentFragment添加到页面
 
var oFragment = document.createDocumentFragment();
 
for(var i = 0 ; i < 10000; i ++) {
 
var p = document.createElement("p");
 
var oTxt = document.createTextNode("段落" + i);
 
p.appendChild(oTxt);
 
oFragment.appendChild(p);
 
}
 
document.body.appendChild(oFragment);

  

jQuery的 append等方法内部也是经过createDocumentFragment来实现的,最好在循环外一次性批量添加DOM元素。 

 
//不建议的作法
 
 
 
varbrowserList = ["IE", "Mozilla Firefox", "Safari", "Chrome", "Opera"];
 
 
 
$.each(browserList, function (index,value) {
 
 
 
$('<li>').text(value).appendTo($('ul').eq(0));
 
 
 
})
 
 
 
//好的作法NO1
 
 
 
varbrowserList = ["IE", "Mozilla Firefox", "Safari", "Chrome", "Opera"];
 
 
 
varmyHTML = '';
 
 
 
$.each(browserList, function (index, value) {
 
 
 
myHTML += '<li>' + value + '</li>';
 
 
 
})
 
 
 
$('ul').eq(0).html(myHTML);
 
 
 
//好的做法NO2
 
 
 
var frag= document.createDocumentFragment();
 
 
 
varbrowserList = ["IE", "Mozilla Firefox", "Safari", "Chrome", "Opera"];
 
 
 
$.each(browserList, function (index, value) {
 
 
 
varli = document.createElement("li");
 
 
 
li.textContent = value;
 
 
 
frag.appendChild(li);
 
 
 
})
 
 
 
$('ul').eq(0).append($(frag));

  

2.      使用display:none,先隐藏后显示,只会引发两次reflow和repaint。因display:none的元素不在render tree,对其操做不会引发其余元素的reflow和repaint。

3.      使用cloneNode和replaceChild,引起一次reflow和repaint。

 

阻塞渲染:CSS 与 JavaScript

谈论资源的阻塞时,咱们要清楚,现代浏览器老是并行加载资源。例如,当 HTML 解析器(HTML Parser)被脚本阻塞时,解析器虽然会中止构建 DOM,但仍会识别该脚本后面的资源,并进行预加载。

同时,因为下面两点:

1. 默认状况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。

2. JavaScript 不只能够读取和修改 DOM 属性,还能够读取和修改 CSSOM 属性。

存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建。另外:

1. 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。

2. JavaScript 能够查询和修改 DOM 与 CSSOM。

3. CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。

因此,script 标签的位置很重要。实际使用时,能够遵循下面两个原则:

1. CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。

2. JavaScript 应尽可能少影响 DOM 的构建。

浏览器的发展日益加快(目前的 Chrome 官方稳定版是 61),具体的渲染策略会不断进化,但了解这些原理后,就能想通它进化的逻辑。下面来看看 CSS 与 JavaScript 具体会怎样阻塞资源。

CSS

<style>p{color:red;}</style>
<link rel="stylesheet" href="index.css">

  

这样的 link 标签(不管是否 inline)会被视为阻塞渲染的资源,浏览器会优先处理这些 CSS 资源,直至 CSSOM 构建完毕。

渲染树(Render-Tree)的关键渲染路径中,要求同时具备 DOM 和 CSSOM,以后才会构建渲染树。即,HTML 和 CSS 都是阻塞渲染的资源。HTML 显然是必需的,由于包括咱们但愿显示的文本在内的内容,都在 DOM 中存放,那么能够从CSS 上想办法。

最容易想到的固然是精简 CSS 并尽快提供它。除此以外,还能够用媒体类型(media type)和媒体查询(media query)来解除对渲染的阻塞。

<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

  

第一个资源会加载并阻塞。
第二个资源设置了媒体类型,会加载但不会阻塞,print 声明只在打印网页时使用。
第三个资源提供了媒体查询,会在符合条件时阻塞渲染。

JavaScript

JavaScript的状况比 CSS 要更复杂一些。观察下面的代码:

<p>Do not go gentle into that good night,</p>
<script>console.log("inline")</script>
<p>Old age should burn and rave at close of day;</p>
<script src="app.js"></script>
<p>Rage, rage against the dying of the light.</p>
 
<p>Do not go gentle into that good night,</p>
<script src="app.js"></script>
<p>Old age should burn and rave at close of day;</p>
<script>console.log("inline")</script>
<p>Rage, rage against the dying of the light.</p>

  

这样的 script 标签会阻塞 HTML 解析,不管是否是 inline-script。上面的 P 标签会从上到下解析,这个过程会被两段JavaScript 分别打断一次(加载而且执行的时间段内)。

因此实际工程中,咱们经常将资源放到文档底部。

改变阻塞模式:defer 与 async

为何要将 script 加载的 defer 与 async 方式放到后面呢?由于这两种方式是的出现,全是因为前面讲的那些阻塞条件的存在。换句话说,defer 与 async 方式能够改变以前的那些阻塞情形。

首先,注意 async 与 defer 属性对于 inline-script 都是无效的,因此下面这个示例中三个 script 标签的代码会从上到下依次执行。

<!-- 按照从上到下的顺序输出 1 2 3 -->
<script async>
  console.log("1");
</script>
<script defer>
  console.log("2");
</script>
<script>
  console.log("3");
</script>

  

 

故,下面两节讨论的内容都是针对设置了 src 属性的 script 标签。

defer

<script src="app1.js" defer></script>
<script src="app2.js" defer></script>
<script src="app3.js" defer></script>

  

defer属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未中止解析,这两个过程是并行的。整个document 解析完毕且 defer-script 也加载完成以后(这两件事情的顺序无关),会执行全部由 defer-script 加载的JavaScript 代码,而后触发 DOMContentLoaded 事件。

defer不会改变 script 中代码的执行顺序,示例代码会按照 一、二、3 的顺序执行。因此,defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成以后。

async

<script src="app.js" async></script>
<script src="ad.js" async></script>
<script src="statistics.js" async></script>

  

async属性表示异步执行引入的 JavaScript,与 defer 的区别在于,若是已经加载好,就会开始执行——不管此刻是HTML 解析阶段仍是 DOMContentLoaded 触发以后。须要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发以前或以后执行,但必定在 load 触发以前执行。

从上一段也能推出,多个 async-script 的执行顺序是不肯定的。值得注意的是,向 document 动态添加 script 标签时,async 属性默认是 true,下一节会继续这个话题。

document.createElement

使用 document.createElement 建立的 script 默认是异步的,示例以下。

console.log(document.createElement("script").async);// true

  

因此,经过动态添加 script 标签引入 JavaScript 文件默认是不会阻塞页面的。若是想同步执行,须要将 async 属性人为设置为 false。

若是使用 document.createElement 建立 link 标签会怎样呢?

conststyle=document.createElement("link");
style.rel="stylesheet";
style.href="index.css";
document.head.appendChild(style);// 阻塞?

  

其实这只能经过试验肯定,已知的是,Chrome 中已经不会阻塞渲染,Firefox、IE 在之前是阻塞的,如今会怎样我没有试验。

document.write 与 innerHTML

经过 document.write 添加的 link 或 script 标签都至关于添加在 document 中的标签,由于它操做的是 document stream(因此对于 loaded 状态的页面使用 document.write 会自动调用 document.open,这会覆盖原有文档内容)。即正常状况下, link 会阻塞渲染,script 会同步执行。不过这是不推荐的方式,Chrome 已经会显示警告,提示将来有可能禁止这样引入。若是给这种方式引入的 script 添加 async 属性,Chrome 会检查是否同源,对于非同源的 async-script 是不容许这么引入的。

若是使用 innerHTML 引入 script 标签,其中的 JavaScript 不会执行。固然,能够经过 eval() 来手工处理,不过不推荐。若是引入 link 标签,我试验过在 Chrome 中是能够起做用的。另外,outerHTML、insertAdjacentHTML() 应该也是相同的行为,我并无试验。这三者应该用于文本的操做,即只使用它们添加 text 或普通 HTML Element。

  

参考:

http://www.cnblogs.com/Peng2014/p/4687218.html

http://www.javashuo.com/article/p-kqratyvy-cr.html

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/

https://zhuanlan.zhihu.com/p/29418126 

相关文章
相关标签/搜索