回流与重绘

在讨论“回流和重绘”之前,我们需要先了解一下浏览器的渲染流程:

  1. 解析 HTML,生成 DOM 树
  2. 解析 CSS,生成 CSSOM 树
  3. 合并 DOM 树和 CSSOM 树,去掉不可见元素,生成渲染树(Render Tree)
  4. Layout(布局):根据生成的渲染树,进行布局,计算得到节点的几何信息(位置,大小)
  5. Painting(绘制):根据渲染树以及节点的几何信息,将渲染树的每个像素渲染到屏幕上

img

为了构建渲染树,浏览器主要完成了以下工作:

  1. 从 DOM 树的根节点开始遍历每个可见节点

    • 某些节点不会被渲染输出,比如scriptlinkmeta等标签
    • 通过 CSS 隐藏的节点会被忽略,比如display: none;。但是通过visibility: hidden; 隐藏的节点还是会被渲染的,因为它仍占据布局空间
  2. 对于每个可见节点,找到 CSSDOM 树中对应的规则,并应用它们

  3. 根据每个可见节点以及对应的样式,组合生成渲染树

回流(reflow)

当渲染树中的节点的尺寸,边距等信息发生改变时,就需要重新计算各节点的几何信息,这个过程就叫做回流。

会触发回流的操作:

  • 盒模型相关属性变化:widthheightmargindisplayborder

  • 定位属性以及浮动等相关属性变化:toppositionfloat

  • 节点内部文字结构发生变化:text-alignoverflowfont-sizeline-heightvertical-align

  • 页面首次渲染

  • 添加或删除可见的 DOM 元素,进行 DOM 操作

  • 节点中的文本或图片尺寸变化

  • 浏览器的窗口尺寸变化

  • CSS 伪类激活,例如:hover

  • 获取布局信息的操作:offsetWidthoffsetHeightclientWidthclientHeightwidthheightscrollTopscrollHeight,getComputedStyle(), getBoundingClientRect()

重绘(repaint)

当元素的外观发生改变时,例如visibilitycolorbackground-color等属性的改变,不会影响到布局。浏览器会根据元素的新属性重新绘制,是元素呈现新的外观,这个过程叫做重绘。

根据浏览器的渲染流程可知:回流一定会触发重绘,但重绘不一定会回流

性能优化

回流和重绘的代价是很昂贵的,因此要尽可能减少回流和重绘发生的次数。

合并多次样式修改

合并多次样式的修改,然后一次性处理掉,例如:

const box = document.querySelector(".box");
box.style.width = "50px";
box.style.height = "50px";
box.style.border = "1px solid #000"

上面的例子中三个样式属性被改变,会触发三次回流。我们可以合并这三次操作,然后统一处理:

const box = document.querySelector(".box");
box.style.cssText = "width: 50px;height: 50px;border: 1px solid #000;";

最好的方式是给这几个样式定义一个类名,然后给元素添加这个类名:

const box = document.querySelector(".box");
box.classList.add("whb");
批量操作DOM

下面的例子中每次给别添加一个li,都会触发一次回流:

const arr = ["html", "css", "javascript"];
const list = document.querySelector("#list");
for(let i = 0; i < arr.length; i++) {
	let li = document.createElement("li");
	li.innerText = arr[i];
	list.appendChild(li);
}

可以使用documentFragment文档片段,先把所有的li插入到文档片段中,再将文档片段插入ul中:

const arr = ["html", "css", "javascript"];
const list = document.querySelector("#list");
const fragment = document.createDocumentFragment();
for(let i = 0; i < arr.length; i++) {
	let li = document.createElement("li");
	li.innerText = arr[i];
	fragment.appendChild(li);
}
list.appendChild(fragment);
减少或避免强制同步布局

在访问某些属性(offsetWidth那一堆属性)时,会导致浏览器强制清空队列,进行强制同步布局。实际使用中应该尽量减少或避免。

比如我们想批量设置一些元素的宽度为某个 box 的宽度:

for(let i = 0; i < elements.length; i++) {
	elements[i].style.width = box.offsetWidth + "px";
}

我们可以把读取到的offsetWidth属性值进行缓存:

const boxWidth = box.offsetWidth;
for(let i = 0; i < elements.length; i++) {
	elements[i].style.width = boxWidth + "px";
}
使用transformopacity来实现动画

比如使用translate代替lefttop。使用opacity代替visibility等。

动画效果应用在 position 属性为 absolute 或 fixed 的元素上

使用绝对定位,避免父元素以及后续元素的频繁回流。

尽可能在 DOM 树的最末端改变 class

在最末端改变class,可以使其影响尽可能少的节点。

避免使用 table 布局

浏览器使用的是流式布局模型,对于渲染树的计算通常只需要遍历一次就可以完成,但table以及内部的元素可能需要多次计算。

参考资料:

https://www.cnblogs.com/xiahj/p/11777786.html

https://www.zhangxinxu.com/wordpress/2010/01/%E5%9B%9E%E6%B5%81%E4%B8%8E%E9%87%8D%E7%BB%98%EF%BC%9Acss%E6%80%A7%E8%83%BD%E8%AE%A9javascript%E5%8F%98%E6%85%A2%EF%BC%9F/?shrink=1