浏览器的渲染过程
从上面这个图上, 我们可以看到, 浏览器渲染过程如下
解析 html 生成 DOM 树, 解析 CSS 生成 CSSOM 树
将 DOM 树和 CSSOM 树结合生成渲染树 renderTree
Layout(回流): 根据生成的渲染树, 进行回流(Layout), 得到节点的几何信息(位置, 大小)
Painting(重绘): 根据渲染树以及回流得到的几何信息, 得到节点的绝对像素
Display: 将像素发送给 GPU, 展示在页面上.
生成渲染树(RenderTree)
为了构建渲染树, 浏览器主要完成了以下工作
从 DOM 树的根节点开始遍历每个可见节点.
对于每个可见的节点, 找到 CSSOM 树中对应的规则, 并应用它们.
根据每个可见节点以及其对应的样式, 组合生成渲染树.
第一步中, 既然说到了要遍历可见的节点, 那么我们得先知道, 什么节点是不可见的. 不可见的节点包括:
一些不会渲染输出的节点, 比如
script,meta,link
等.
一些通过 CSS 进行隐藏的节点. 比如 display:none. 注意, 利用 visibility 和 opacity 隐藏的节点, 还是会显示在渲染树上的. 只有 display:none 的节点才不会显示在渲染树上.
回流(Layout)
前面我们通过构造渲染树, 我们将可见 DOM 节点以及它对应的样式结合起来, 可是我们还需要计算它们在设备视口 (viewport) 内的确切位置和大小, 这个计算的阶段就是回流.
为了弄清每个对象在网站上的确切大小和位置, 浏览器从渲染树的根节点开始遍历, 而在回流这个阶段, 我们就需要根据视口具体的宽度, 将其转为实际的像素值
重绘(Painting)
通过回流 (Layout) 阶段, 我们知道了所有的可见节点的样式和具体的几何信息(位置, 大小), 那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素, 这个阶段就叫做重绘节点.
何时发生回流重绘
回流阶段是计算节点的几何信息和位置, 那么当页面布局或者几何信息发生改变时, 就需要回流.
添加或者删除可见的 DOM 元素
元素的位置, 尺寸发生变化
页面开始渲染的时候(这肯定避免不了)
浏览器的视口尺寸大小发生改变(因为回流是根据浏览器视口的大小来计算元素的位置和尺寸大小)
注意: 回流一定会触发重绘, 而重绘 (非几何信息的样式发生改变) 不一定会回流, reflow 回流的成本开销要高于 repaint 重绘, 一个节点的回流往往回导致子节点以及同级节点的回流;
根据改变的范围和程度, 渲染树中或大或小的部分需要重新计算, 有些改变会触发整个页面的重排, 比如, 滚动条出现的时候或者修改了根节点.
基于回流 (Layout), 重绘(Painting) 的优化方法
避免扰乱现代浏览器的优化机制
在现代浏览器的中, 由于每次回流, 重绘的时候, 都需要额外的计算消耗, 因此会通过队列化修改, 并批量执行来优化这一过程. 浏览器会将修改操作放入队列里面, 直到过了一段时间或者达到一个阈值, 才清空队列.
但是当你获取布局信息时, 会强制刷新队列, 例如:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- getComputedStyle()
- getBoundingClientRect()
上面这些方法, 都需要获取最新的布局信息, 所以浏览器会强制刷新队列并执行回流, 重绘, 来获取最新的信息.
因此我们在修改样式的时候, 应该尽量避免使用上面的属性, 方法, 如果非要使用, 可以先缓存起来然后一起获取.
CSS 的修改方式
考虑以下代码
- const el = document.getElementById('el')
- el.style.padding = 'xxx'
- el.style.margin = 'xxx'
- el.style.border = 'xxx'
这里元素的几何信息有三次被修改了, 但是现代浏览器会将起缓存起来, 但是如果这期间有通过前面列出来的属性, 方法访问位置信息的话就会触发三次回流, 重绘. 所以还是建议通过 cssText 或者 class 的方法一次性修改.
- el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
- // 或者
- el.className += 'xxx';
批量修改 DOM
当我们需要对 DOM 进行一系列修改的时候, 可以通过以下几种方式减少回流重绘次数:
隐藏元素, 应用修改, 重新显示
- function appendDataToElement (appendToElement, data) {
- let li;
- for ( let i = 0; i <data.length; i++) {
- li = document.createElement('li');
- li.textContent = 'text';
- appendToElement.appendChild(li);
- }
- }
- const ul = document.getElementById('list');
- ul.style.display = 'none'; // 首先脱离文档流
- appendDataToElement(ul, data);
- ul.style.display = 'block'; // 操作完以后再可见
使用文档片段 (document fragment) 在当前 DOM 之外构建一个子树, 再把它拷贝回文档.
- const ul = document.getElementById('list');
- const fragment = document.createDocumentFragment()
- appendDataToElement(fragment , data);
- ul.appendChild(fragment )
对于复杂动画效果, 使用绝对定位让其脱离文档流
对于复杂动画效果, 由于会经常的引起回流重绘, 因此, 我们可以使用绝对定位, 让它脱离文档流. 否则会引起父元素以及后续元素频繁的回流.
CSS 与 JS 是这样阻塞 DOM 解析和渲染的
通过 < script > 与 < link > 引入外部资源, 当解析到该标签的时候, 会进行下载.
CSS 脚本的加载不会阻塞 DOM 解析过程, 但是会阻塞渲染过程(painting)
JS 脚本的加载会阻塞 DOM 解析过程
JS 脚本的加载中, 如果你确定没必要阻塞 DOM 解析的话, 不妨按需要加上 defer 或者 async 属性, 此时脚本下载的过程中是不会阻塞 DOM 解析的.
浏览器遇到 <script > 且没有 defer 或 async 属性的标签时, 为了为 < script > 标签内部的 JS 提供最新的信息, 会触发页面的回流, 重绘过程. 因而如果前面 CSS 资源尚未加载完毕时, 浏览器会等待它加载完毕之后再执行脚本.
所以 < script > 最好放底部(防止阻塞 DOM 解析).<link > 最好放头部(为渲染过程提供样式). 如果头部同时有 < script > 与 < link > 的情况下, 最好将 < script > 放在 < link > 上面(为了防止 CSS 脚本加载时间过长, 使 JS 等待时间也很长)
来源: https://segmentfault.com/a/1190000017506726