Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浏览器渲染过程 #35

Open
andyChenAn opened this issue Mar 27, 2019 · 0 comments
Open

浏览器渲染过程 #35

andyChenAn opened this issue Mar 27, 2019 · 0 comments
Labels
browser 浏览器相关

Comments

@andyChenAn
Copy link
Owner

浏览器渲染过程

  • 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>
@andyChenAn andyChenAn added the browser 浏览器相关 label Mar 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
browser 浏览器相关
Projects
None yet
Development

No branches or pull requests

1 participant