Skip to content

浏览器渲染过程 #35

Open
Open
@andyChenAn

Description

@andyChenAn

浏览器渲染过程

  • 1、解析HTML,构建DOM树
  • 2、解析CSS,构建CSSOM树
  • 3、将DOM树和CSSOM树合并成一个渲染树
  • 4、根据渲染树来进行布局,计算每个节点的几何信息
  • 5、将各个节点绘制到屏幕上

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

  • 1、从DOM树的根节点开始遍历每个可见节点。
    • 某些节点不可见(比如script,meta标签等),因为它们不会体现在渲染输出中,所以会忽略。
    • 某些节点通过CSS隐藏,因此在渲染树中也会被忽略,比如一个节点设置了“dispaly:none”属性
  • 2、对于每个可见节点,为其找到适配的CSSOM规则并应用它们。
  • 3、根据每个可见节点以及它们对应的样式,组合生成渲染树。

注意的是:渲染树只包含可见节点

回流

浏览器将DOM树和CSSOM树组合成渲染树之后,还需要计算节点在设备视口内的确切位置和大小,这个计算的阶段就是回流。

为弄清每个节点在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。我们来看一下例子:

<!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%">Hello world!</div>
    </div>
  </body>
</html>

我们可以看到第一个div将节点的显示尺寸设置为视口宽度的50%,父div包含的第二个div将其宽度设置为其父节点宽度的50%,即视口的25%。而在回流阶段,我们就需要根据视口宽度,将其转为具体的像素值。

image

重绘

既然我们知道了哪些可见节点,它们的计算样式以及几何信息,我们终于可以将这些信息传递给最后一个阶段,将渲染树上的每个节点转换成屏幕上的实际像素。这一步通常被称为:重绘。

什么时候会发送回流重绘

我们了解到,回流阶段主要是计算节点的位置或几何信息,那么当页面布局和几何信息发生变化的时候,就需要进行回流。比如:

  • 1、添加或删除可见DOM元素
  • 2、DOM元素的位置发生变化
  • 3、DOM元素的尺寸大小发生变化
  • 4、DOM元素的内容发生变化
  • 5、浏览器的窗口尺寸发生变化

这里需要注意的是:回流一定会导致重绘,但是重绘不一定会导致回流

当我们获取DOM节点的布局信息时,也会导致浏览器回流重绘。比如:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight、width、height等

可以访问这里去查看:

当我们获取节点的这些属性值时,都会导致浏览器回流重绘,因为当我们获取这些值的时候,需要返回最新的值,所以浏览器会触发回流,重新计算布局信息,来返回正确的值。

减少回流和重绘

1、合并多次对DOM样式的修改

let box = document.getElementById('box');
box.style.padding = '10px';
box.style.borderTop = '20px';
box.style.width = '100px';

向上面的例子中,每次重新设置节点的样式都会导致浏览器回流重绘,所以最好是将修改样式的代码合并成一句,比如:

box.style.cssText = 'padding:10px;borderTop : 20px;width:100px';

或者也可以通过添加一个样式类,来达到这样的效果。

批量修改DOM

当我们需要对DOM进行一系列修改的时候,我们可以这样来减少回流重绘次数:使元素脱离文档流,然后对其进行多次修改,最后将元素放回到文档中。

这里有三种方式可以使DOM脱离文档流:

  • 隐藏元素,然后对元素修改,最后再重新显示
  • 使用文档片段(document Fragment)
  • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素

如果我们需要通过循环,批量插入节点到文档中,一般的做法都是:

let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};

appendToList(list , data);

但是每一次插入节点的时候都会引起浏览器回流重绘,所以我们可以使用这三种方式来进行优化:

隐藏元素
let list = document.getElementById('list');
list.style.display = 'none';
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};

appendToList(list , data);
list.style.display = 'block';
使用文档片段
let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};
let fragment = document.createDocumentFragment();
appendToList(fragment , data);
list.appendChild(fragment);
将原始元素拷贝到一个脱离文档的节点中
let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];

let appendToList = function (target , data) {
    for (let i = 0 ; i < data.length ; i++) {
        let li = document.createElement('li');
        li.innerHTML = data[i];
        target.appendChild(li);
    }
};
let clone = list.cloneNode(true);
appendToList(clone , data);
list.parentNode.replaceChild(clone , list);

对于一些复杂的动画,可以使用绝对定位使其脱离文档流

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
    
    #box {
      background: red;
      animation: scale 4s linear 0s infinite alternate;
      background-image: linear-gradient(to right, rgba( 0,0,0,0.9 ) 25%, rgba( 0,0,0,0.1 ) 50%, rgba( 0,0,0,0.9 ) 75%);
      will-change: all;
      transform: translate3d(0, 0, 0);
    }

    @keyframes scale {
      from { 
        width: 100px; 
        height: 100px;
        background: red;
        margin: 10px;
        transform: rotate(0);
        margin-left: -20%;
        rotate: 10deg;
      }
      to {
        width: 200px;
        height: 200px;
        background: yellow;
        margin: 50px;
        transform: rotate(360deg);
        margin-left: 100%;
      }
    }
    </style>
</head>
<body>
    <div id="box"></div>
    <button id="btn">click</button>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
    <p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<script>

let box = document.getElementById('box');
btn.addEventListener('click' , function () {
    box.style.position = 'absolute';
});

let frame = 0;
let start = null;
function loop (timestamp) {
    if (!start) {
        start = timestamp;
    }
    frame++;
    let diff = timestamp - start;
    if (diff > 1000) {
        console.log('1秒内刷新频率为:' + Math.round((frame * 1000) / diff));
    }
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop)
</script>
</body>
</html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    browser浏览器相关

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions