浏览器渲染流程、优化技巧

浏览器渲染流程

  1. 🌲 解析HTML生成DOM树
  2. 🌲 解析CSS生成CSSOM树。
  3. 🌲 DOM树、CSSOM树合并生成渲染树(Render Tree)
  4. 🧙‍♂️ 回流Reflow)。
  5. 👨‍🎨 重绘Repaint)。
  6. 😎 GPU绘制页面。

渲染树 (Render Tree

#

💡 渲染树的生成,对应的是浏览器渲染的1-3步骤(1.HTML、2.CSS、3.Render Tree)。

  1. DOM树根节点循环遍历每一个可见节点

    🚨 不可见节点:scriptmetalink

  2. 对于每个可见节点,找到CSSOM树对应规则运用

    🚨 不可见属性:display:none

  3. 根据可见节点和样式,生成Render树

  4. 最终,渲染树只包含可见节点

回流 (Reflow )

💡 回流,是浏览器计算各种元素在设备视口的位置和大小,这个阶段也是最消耗资源的阶段。

<!-- 例:-->
<!-- 例如以下HTML代码,父div标签,宽度为50%,子div标签为父div标签的50%。-->
<!-- 回流阶段就会根据视口的宽度,将各个元素的50%,转换为实际的像素长度。-->
<!DOCTYPE html>
<html>

<head>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Critial Path: Hello world!</title>
</head>

<body>
  <div style="width: 50%">
    <div style="width: 50%">什么是回流?</div>
  </div>
</body>

</html>
导致回流的原因
  • 🚨 页面初始化
  • 🚨 添加、删除DOM元素
  • 🚨 元素位置变化
  • 🚨 元素尺寸变化
  • 🚨 元素内容变化 (包括text文本、图片变化)
  • 🚨 浏览器窗口大小变化

重绘 (Repaint

经过回流阶段,已经能确定Render树结构、以及元素对应的位置样式

重绘阶段,一些元素需要更新属性,而这些属性只是影响元素的外观,而不会影响布局的,比如background-color,则称之为重绘。

如何减少回流、重绘

💡 回流导致额外的资源损耗浏览器通过队列化批量执行来优化重绘过程。通常是将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才执行并清空队列(除强制回流API)。以下罗列一些常见的优化技巧。 

(1)🙋‍♂️ 减少使用强制回流API

         以下API尽量少用、不用,否则会导致强制回流。

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • window.scrollX、window.scrollY
  • element.focus()
  • mouseEvt.offsetX、mouseEvt.offsetY
  • window.getComputedStyle()
  • ......

(2)🙋‍♂️ 合并样式操作

同时对元素进行多次操作,会导致几何结构变化引起回流

🐢 优化前 回流多次

// 老版本浏览器(未优化)没有批量操作队列,会触发三次回流
const el = document.getElementById('test');
el.style.padding = '5px'; // 🐢 第一次操作,回流
el.style.borderLeft = '1px'; // 🐢 第二次操作,回流
el.style.borderRight = '2px'; // 🐢 第三次操作,回流

🐇 优化方案(2.1)

// cssText 通过cssText属性改变样式 回流一次
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

🐇 优化方案(2.2)

// class 直接改class 回流一次
const el = document.getElementById('test');
el.className += ' active';

 

(3)🙋‍♂️ 批量操作DOM

    当需要对DOM进行一系列修改时,可以通过以下步骤进行优化:

  1. ☝️脱离文档流 (引起回流)
  2. ☝️对元素进行批量操作
  3. ☝️将元素带回文档流 (引起回流)

🐢 优化前 回流多次

// 该操作会导致浏览器多次“回流”
const ul = document.getElementById('ulElement');
// 循环添加5次li元素
for (let i = 0; i < 5; i++) {
  const li = document.createElement("li");
  li.textContent = "text";
  ul.appendChild(li);
};

🐇 优化方案(3.1)

// 🛠 先隐藏目标元素,完成操作后在显示,该方案固定回流两次
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"; // 1.先隐藏ul元素 回流+1
appendDataToElement(ul, data); // 2.ul元素添加多个li
ul.style.display = "block"; // 3.添加完成后 在显示 回流+1

🐇 优化方案(3.2)

// 🛠 使用文档片段,在DOM外创建一个子树,在拷贝回原文档
const ul = document.getElementById("list");
const fragment = document.createDocumentFragment(); // 当前DOM外创建一个子树🌲
appendDataToElement(fragment, data); // 循环添加
ul.appendChild(fragment); // copy回原文档 回流+1

🐇 优化方案(3.3)

// 🛠 拷贝原元素,操作拷贝元素,在执行替换,性能较差
const ul = document.getElementById("list");
const clone = ul.cloneNode(true); // copy元素
appendDataToElement(clone, data); // 循环添加
ul.parentNode.replaceChild(clone, ul); // 替换 回流+1

 

(4)🙋‍♂️ 复杂动画脱离文档流

// 脱离文档流 使其与其他元素互不联系
function update() {
  const box = document.getElementById("box");
  box.style.position = "absolute";
}

 

(5)🙋‍♂️ CSS3 硬件加速

🔍 避免回流的硬件加速css属性:

  • transform
  • opacity
  • filters
  • Will-change

👀 缺点

  • 内存占用量较大
  • 在GPU渲染字体会导致抗锯齿无效