前言

当我们输入 juejin.cn 再按下回车键,我们就开始了我们的之旅了。

从输入https://juejin.cn到页面渲染之间的那些事(附带字节面试题)

虽然我们只是简简单单的输入了一个URL,但是在按下回车键的那一刹那,一系列复杂的过程就开始了,浏览器在这之中默默无闻的做了很多的事情,最终才将页面呈现在我们的面前。

主要步骤

  1. 解析html代码,生成一个DOM树
  2. 解析css,生成 CSSOM 树
  3. 将DOM树和CSSOM树结合,去除不可见的元素,生成 Render Tree
  4. 计算布局(回流|重排),根据Render Tree进行布局的计算,得到每一个节点的几何信息
  5. 绘制页面(重绘),GPU根据布局信息绘制

这就来到我们这篇文章的重点了,什么是回流?什么是重绘?如何减少回流重绘,优化浏览器的性能呢?

回流(Reflow)

回流(Reflow)是指浏览器为了重新绘制部分或整个页面而重新计算元素的位置和几何结构的过程。当页面布局发生改变时,浏览器需要重新计算元素的布局信息,并且根据新的布局信息进行重新绘制,这个过程就是回流。

什么时候会发生回流?

常见触发回流的操作包括:

  1. 修改元素的尺寸(宽度、高度)或位置;
  2. 修改元素的内容(文字内容、图片等);
  3. 添加或删除可见的DOM元素;
  4. 改变浏览器窗口的大小;

总的来说:当一个容器的几何属性发生变更时,页面会发生回流

重绘(Repaint)

重绘(Repaint)是指浏览器根据元素的样式和内容对页面进行重新绘制的过程,而不涉及到元素的布局改变,即将已经计算好布局的容器在屏幕上展现出来。

什么时候会发生重绘?

常见触发重绘的操作包括:

  1. 修改元素的颜色、背景、文字样式等视觉属性;
  2. 使用 CSS3 的 transform 和 opacity 属性;
  3. 添加或移除 CSS 类;
  4. 在元素上触发 CSS 动画;

总的来说:当元素的非几何属性变化时,会发生重绘

总结:

回流一定重绘,重绘不一定回流

当元素的样式发生改变,但不影响其在文档流中的位置时,浏览器只需要重新绘制受影响的部分,而不需要重新计算元素的布局信息。而回流发生时,一定有元素的位置或几何结构发生变化,其外观也会受到影响,就需要重新绘制。

如何减少回流重绘,优化浏览器的性能

浏览器的优化策略

渲染队列: 当页面中的元素发生样式变更时,浏览器会将这些变更放入渲染队列中。如果在短时间内发生了多次样式变更,浏览器会将它们合并成一个单独的回流操作。这意味着,即使有多个样式变更,浏览器也会批量化执行渲染队列中的回流过程,从而减少了不必要的性能损耗。

虽然浏览器会尽可能地将多次样式变更合并成一个批量操作,但有些操作仍然会强制触发渲染队列的执行,导致回流和重绘的发生。

  1. 获取布局信息: 当使用offsetTopoffsetLeftoffsetWidthoffsetHeight等属性来获取元素的布局信息时,浏览器会立即执行渲染队列以确保获取到最新的布局信息。
  2. 获取样式信息: 当使用getComputedStyle()等方法获取元素的样式信息时,浏览器也会立即执行渲染队列。
  3. 修改某些样式属性: 有些样式属性的修改会立即触发渲染队列的执行,例如scrollLeftscrollTop的修改,以及在CSS动画中使用的requestAnimationFrame()等。
  4. 强制重绘: 使用像element.offsetWidthelement.offsetHeightelement.style.display = 'none'等操作可以强制浏览器执行重绘操作,但不会触发回流。
  5. JavaScript执行的结束: 当JavaScript执行结束时,浏览器会检查是否需要执行渲染队列,然后进行回流和重绘操作。

如何减少回流重绘

  1. 合理利用浏览器的优化策略
  2. 先dispaly:none; 修改完样式后, 再block回来

来个场景来展示下如何减少回流重绘:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <ul id="box"></ul>
  <script>
    const ul = document.getElementById("box");
    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      let text = document.createTextNode(i)
      li.appendChild(text)
      ul.appendChild(li);
    }
  </script>

在这个场景中,会发生100次回流,每次添加一个新的<li>元素到<ul>中,会发生一次回流。

方案一:先dispaly:none; 修改完样式后, 再block回来

  <script>
    const ul = document.getElementById("box");
    ul.style.display = "none";
    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      let text = document.createTextNode(i)
      li.appendChild(text)
      ul.appendChild(li);
    }
    ul.style.display = "block"
  </script>

方案二:使用虚拟文档片段

  <script>
    const ul = document.getElementById("box");
    const fragment = document.createDocumentFragment() // 虚拟的文档片段
    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      let text = document.createTextNode(i)
      li.appendChild(text)
      fragment.appendChild(li);
    }
    ul.appendChild(fragment)
  </script>

文档片段是一个虚拟的DOM容器,它允许你在内存中创建和操作DOM节点,而不会直接影响到页面的渲染。与普通的文档节点不同,文档片段不属于文档树的一部分,但可以用来暂时保存一组DOM节点。

文档片段的主要优势在于,当将其附加到文档中时,它的内容会一次性地被插入到文档中,而不会引发多次重排和重绘,因此可以提高性能。

方案三:克隆(更高级且优雅)

  <script>
    const ul = document.getElementById("box");
    const clone = ul.cloneNode(true) // 克隆一份ul,深拷贝
    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      let text = document.createTextNode(i)
      li.appendChild(text)
      clone.appendChild(li);
    }
    ul.parentNode.replaceChild(clone, ul)
  </script>

通过替换而不是直接修改DOM来减少回流,这是很优雅的一个方案。

除了方案一会造成两次回流,其他方案都是只造成一次回流,都减少了页面上的回流重绘。

总结

看完这篇文章,相信各位小伙伴都会有所收获的吧,那么最后就再来个字节面试题对知识加深下理解吧

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script>
    let el = document.getElementById('app')
    el.style.width = (el.offsetWidth + 1) + 'px'
    el.style.width = 1 + 'px'
  </script>
</body>
</html>

这里发生了几次的回流重绘?

答案是造成一次回流重绘

我们来看JS代码,首先是获取了ID为”app”的元素,然后进行了两次样式操作。

  1. 第一次操作是将元素的宽度设置为 el.offsetWidth + 1。当遇见el.offsetWidth时,会强制触发渲染队列的执行,但这时,渲染队列中是没有任何操作的,即页面中没有元素发生样式变更,并不会触发回流重绘,接下来就是将 el.style.width = (el.offsetWidth + 1) + 'px'放进队列。
  2. 第二次操作就是将 el.style.width = 1 + 'px'放进队列。
  3. 最后批量化执行渲染队列中的回流过程,所以只进行一次回流重绘

这个算是字节面试题中的简单题了,相信大家都能轻而易举的拿下,最后祝你也祝我在今后日子里能够登高望远,心向彼岸。