简要概述游览器如何渲染页面

245 阅读5分钟

浏览器解析渲染页面

  1. 解析HTML,构建DOM树
  2. 解析CSS,生成CSS规则树
  3. 合并DOM树和CSS规则,生成render树
  4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  5. 绘制render树(paint),绘制页面像素信息
  6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上

image.png

构建DOM树

字节 → 字符 → 令牌 → 节点 → 对象模型

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

image.png

注意:

  1. Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符
  2. Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集
  3. Lexing词法分析:将token转换为对象,这些对象分别定义他们的属性和规则
  4. DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样

生成CSS规则

Bytes → characters → tokens → nodes → CSSOM

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

构建渲染树(render树)

当DOM树和CSSOM都有了后,就要开始构建渲染树了

一般来说,渲染树和DOM树相对应的,但不是严格意义上的一一对应

因为有一些不可见的DOM元素不会插入到渲染树中,如head这种不可见的标签或者display: none

image.png

渲染

有了render树,接下来就是开始渲染,基本流程如下:

  1. 计算CSS样式
  2. 构建渲染树
  3. 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性
  4. 绘制,将图像绘制出来

image.png

然后,图中的线与箭头代表通过js动态修改了DOM或CSS,导致了重新布局(Layout)或渲染(Repaint)

这里Layout和Repaint的概念是有区别的:

  • Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
  • Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了

回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。

重绘和重排

重绘:

元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色边框颜色文字颜色等),浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。

注意:table及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用table布局页面的原因之一。

重排:

当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。

重绘和重排的关系:

在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,重排必定会引发重绘,但重绘不一定会引发重排。

引起重排重绘的原因有:

  1. 添加或删除可见的DOM元素
  2. 元素位置改变
  3. 元素本身的尺寸发生改变
  4. 内容改变
  5. 浏览器页面初始化
  6. 浏览器窗口大小发生改变

减少重绘重排的方法有:

css:

  1. 避免使用 table 布局。
  2. 尽可能在 DOM 树的最末端改变 class。
  3. 避免设置多层内联样式。
  4. 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。
  5. 避免使用 CSS 表达式(例如:calc())。

js:

  1. 改变样式

    使用csstext,className一次性改变属性

    // bad
    var el = document.querySelector('.el');
    el.style.borderLeft = '1px';
    el.style.borderRight = '2px';
    el.style.padding = '5px';
    
    //good
    var el = document.querySelector('.el');
    el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
    
    // css 
    .active {
        padding: 5px;
        border-left: 1px;
        border-right: 2px;
    }
    // javascript
    var el = document.querySelector('.el');
    el.className = 'active';
    
  2. 批量修改DOM

    核心思想:

    • 让该元素脱离文档流
    • 对其进行多重改变
    • 将元素带回文档中

    分类:

    • 隐藏元素,进行修改后,然后再显示该元素 display=none使元素脱离文档流

      let ul = document.querySelector('#mylist');
      ul.style.display = 'none';
      appendNode(ul, data);
      ul.style.display = 'block';
      
    • 使用文档片段fragment创建一个子树,然后再拷贝到文档中

      let fragment = document.createDocumentFragment();
      appendNode(fragment, data);
      ul.appendChild(fragment);
      
    • 将原始元素拷贝clone到一个独立的节点中,操作这个节点,然后覆盖原始元素

      let old = document.querySelector('#mylist');
      let clone = old.cloneNode(true);
      appendNode(clone, data);
      old.parentNode.replaceChild(clone, old);
      
  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. 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。