[前端] 重排和重绘

网页的生成过程

  1. 解析HTML,生成DOM树。
  2. 解析CSS,生成CSSOM树。
  3. 结合DOM树和CSSOM树,为每一个节点计算CSS属性,生成渲染树,RenderTree。
  4. 生成布局(Flow),计算渲染树上所有节点的位置。
  5. 将布局绘制(Paint)到屏幕上。
  • 布局生成和绘制的过程就是渲染。
  • 网页生成的时候至少渲染一次。
  • 用户交互可能导致重新渲染。
  • 渲染是耗时的,应减少不必要的重新渲染以提高网页性能。

重排和重绘的概念

  • 重新生成布局,就叫重排(Reflow),也叫回流。
  • 重新绘制,就是重绘(Repaint)。

由于布局生成和绘制存在先后顺序关系,重排必定导致重绘,但重绘不一定需要重排。

重排 Reflow

重排与布局有关,当布局发生变化时,也就是元素的几何信息(DOM节点的尺寸和位置)发生变化时,将会触发重排,重新计算元素的几何位置,然后重新绘制。

常见引起重排的属性和方法

  • 添加或删除可见的DOM元素;
  • 元素尺寸改变——边距、填充、边框、宽度高度;
  • 内容变化,比如input框中输入文字;
  • 浏览器窗口尺寸改变——resize事件发生时;
  • 读取offsetWidth、offsetHeight等属性(浏览器为确保数据准确性,会先强制重排);
  • 设置style属性的值(改变了元素的尺寸或位置);

重排影响范围

  • 全局范围重排:从根节点html开始对整个渲染树进行重排;

    当一个DOM节点变小,它后续的元素可能位置也发生变化,而它的父元素如果没有固定宽高也可能发生收缩,因此:

    一个节点的重排可能影响到它的相邻节点、父节点......从而引发全局范围的重排。

  • 局部范围重排:对渲染树的某部分或某一个渲染对象进行重排。

    将一个DOM的几何信息固定,当其内部的节点发生重排,则不会影响到外部的节点,是局部范围的重排。

重排总结

  • 重排的性能开销渲染树上需要重新构建的节点数有关。
  • 尽量以局部布局的形式组织html,将重排控制在局部范围内。

重绘 Repaint

一个元素的外观发生改变,但不改变布局,不影响周围的元素,只是重新绘制该元素的外观,这个过程叫做重绘。

常见引起重绘的属性和方法

  • color
  • border-style
  • background
  • visibility
  • border-radius
  • box-shadow
  • outline
  • text-decoration

渲染队列机制

当元素的几何属性被修改,不会立刻导致重排。这个操作会被放到渲染队列,等到队列中的操作到达一定数量或者达到一定的时间间隔,浏览器才会批量执行这些操作。

如下的代码执行了四次对于元素的几何属性的修改,但是只会触发一次重排。

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

强制刷新队列

当访问与布局相关的属性时,浏览器为确保其准确性、准时性,会强制立即执行渲染队列中的任务,即立即重排和重绘。

如下这段代码会触发4次重排和重绘:

div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);

强制刷新队列的属性和方法

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle(), 或者 IE的 currentStyle

在开发过程中应该尽可能少地访问这些属性,避免多次重排。

重排优化策略

1. 分离读写操作

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

统一读,再统一写,而不是边读边写。这样可以将重排次数降低到1次。第一个console.log将渲染队列清空,触发一次重排,而后续的console.log由于渲染队列本身是空的因此不会触发重排。

2. 样式集中改变

不要分批次的修改样式属性,而是一次性修改。

建议提前写好若干个CSS的class选择器,然后 JS 负责切换。

// bad
box.style.left = '10px';
box.style.top = '200px';
box.style.transform = 'scale(1.1)';

// good
box.classList.add('className1');
box.classList.remove('className2');

3. 缓存布局信息

布局信息的每一次读取都会导致强制重排,如果确保布局信息在当前情景下不会有变动,可以使用一个临时变量缓存使用。

// bad 强制刷新 触发两次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';

// good 
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';

4. 离线修改DOM

DOM设置display: none;,将其从渲染树中移除,然后再进行复杂修改操作,最后再将其显示出来。

整个过程包含隐藏和显示共两次重排。

或者使用DocumentFragment创建一个DOM碎片,然后在其上面批量操作DOM,操作完成之后再添加到文档中,这样只会触发一次重排。

5. 目标元素设置绝对定位

设置positionabsolutefixed,这样就只会影响子节点的重排,不会影响外部。

6. 不要使用table布局

table中的某一个元素触发重排将导致整个table重排。

如果是在维护远古项目不得不使用table布局,可以设置table-layout: fixed;或者table-layout: auto;,这样可以让table一行一行的渲染,限制重排的影响范围。

热门相关:诛明   诛明   陆先生偏要以婚相许   霍先生结婚吧   盛世娇宠之名门闺香