浏览器渲染页面原理,reflow、repaint及其优化
浏览器的主要组件包括:
1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的你请求的页面外,其他显示的各个部分都属于用户界面。
2. 浏览器引擎 - 在用户界面和渲染引擎之间传送指令。
3. 渲染引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
4. 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
5. 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
6. JavaScript 解释器。用于解析和执行 JavaScript 代码,比如chrome的JavaScript解释器是V8。
7. 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5)定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
关键路径渲染(Critical Rendering Path):渐进式
浏览器拿到HTML之后的渲染过程:(不同内核实现不一样但大概是这样)
1. 解析HTML,构建DOM tree。
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 中代码的执行顺序,示例代码会按照 1、2、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
https://segmentfault.com/a/1190000014070240
https://developers.google.com/web/fundamentals/performance/critical-rendering-path/
浏览器渲染页面原理,reflow、repaint及其优化的更多相关文章
- 网页性能优化:防止JavaScript、CSS阻塞浏览器渲染页面
网页中引用的外部文件: JavaScritp.CSS 等常常会阻塞浏览器渲染页面.假设在 <head> 中引用的某个 JavaScript 文件由于各种不给力需要2秒来加载,那么浏览器渲染 ...
- 浅谈浏览器解析 URL+DNS 域名解析+TCP 三次握手与四次挥手+浏览器渲染页面
(1)浏览器解析 URL 为了能让我们的知识层面看起来更有深度,我们应该考虑下面两个问题了: 从浏览器输入 URL 到渲染成功的过程中,究竟发生了什么? 浏览器渲染过程中,发生了什么,是不是也有重绘与 ...
- 浏览器渲染页面的时候,不同的script块之间的关系
浏览器渲染页面时,当读到script元素的时候,浏览器中的js引擎会分多个script代码块来读取,不同的script代码出错互不影响,但是由于script中的变量作用域是全局,所以前面代码块声明的变 ...
- chrome和Firefox浏览器渲染页面的不同
一直很好奇chrome和firefox这两大浏览器的页面渲染有什么不同,今天自己写了些html代码来做了下检验. 先做html编码,代码如下: <!DOCTYPE html><htm ...
- 从浏览器渲染层面解析css3动效优化原理
引言 在h5开发中,我们经常会需要实现一些动效来让页面视觉效果更好,谈及动效便不可避免地会想到动效性能优化这个话题: 减少页面DOM操作,可以使用CSS实现的动效不多出一行js代码 使用绝对定位脱离让 ...
- 160826、浏览器渲染页面过程描述,DOM编程技巧以及重排和重绘
一.浏览器渲染页过程描述 1.浏览器解析html源码,然后创建一个DOM树. 在DOM树中,每一个HTML标签都有一个对应的节点(元素节点),并且每一个文本也都有一个对应的节点(文本节点). DO ...
- 浏览器渲染页面过程描述,DOM编程技巧以及重排和重绘。
一.浏览器渲染页过程描述 1.浏览器解析html源码,然后创建一个DOM树. 在DOM树中,每一个HTML标签都有一个对应的节点(元素节点),并且每一个文本也都有一个对应的节点(文本节点). DOM树 ...
- 运用webkit绘制渲染页面原理解决iscroll4闪动的问题
原:http://www.iunbug.com/archives/2012/09/19/411.html 已经有不少前端同行抱怨iScroll4的各种问题,我个人并不赞同将这些问题归咎于iScroll ...
- 【Web动画】CSS3 3D 行星运转 && 浏览器渲染原理
承接上一篇:[CSS3进阶]酷炫的3D旋转透视 . 最近入坑 Web 动画,所以把自己的学习过程记录一下分享给大家. CSS3 3D 行星运转 demo 页面请戳:Demo.(建议使用Chrome打开 ...
随机推荐
- Qt Gui 第一章~第二章
一.Qt启动 qmake -project; 创建xxx.pro qmake xxx.pro; 生成makefile文件 make:构建该程序,生成可执行文件 运行程序:windows:xxx:mac ...
- 菜鸟教程 Missing parentheses in call to 'print'
个人博客 地址:http://www.wenhaofan.com/article/20180618180327 >>> print "hello" SyntaxE ...
- 关于List比较好玩的操作
作为Java大家庭中的集合类框架,List应该是平时开发中最常用的,可能有这种需求,当集合中的某些元素符合一定条件时,想要删除这个元素.如: public class ListTest { publi ...
- Docker的安装和操作(虚拟机+linux系统)
1.简介 Docker是一个开源的应用容器引擎:是一个轻量级容器技术: Docker支持将软件编译成一个镜像:然后在镜像中各种软件做好配置,将镜像发布出去,其他使用者可以直接使用这个镜像: 运行中的这 ...
- 使用TensorFlow训练模型的基本流程
本文已在公众号机器视觉与算法建模发布,转载请联系我. 使用TensorFlow的基本流程 本篇文章将介绍使用tensorflow的训练模型的基本流程,包括制作读取TFRecord,训练和保存模型,读取 ...
- centos7中 yum的安装
自己误将yum卸载, 在重装时由于依赖问题一直报错: error: Failed dependencies: /usr/bin/python is needed by yum-3.4.3-16 ...
- NG-ALAIN 边学边记1
在文件夹下右键启动powerShell ng new my-project --skip-npm cd my-project ng add ng-alainnpm installng serve np ...
- java课后作业3
1.动手动脑 由于类中定义了需要参数的构造方法,导致系统不再提供默认的构造方法. 2.java字段初始化 运行结果 100 300 java字段在初始化时先按照对应的构造方法执行.若构造方法中没有对变 ...
- Java必须知道的知识点
junit用法,before,beforeClass,after, afterClass的执行顺序 分布式锁 nginx的请求转发算法,如何配置根据权重转发 用hashmap实现redis有什么问题( ...
- python3练习100题——003
今天继续-答案都会通过python3测试- 原题链接:http://www.runoob.com/python/python-exercise-example3.html 题目:一个整数,它加上100 ...