浏览器的渲染流程

364 阅读5分钟

浏览器的渲染整体流程

浏览器中的页面,可以看做是一幅幅可以和人互动的神奇的画,而这幅画是由浏览器绘制出来的,浏览器绘制这幅画的过程称之为渲染。 渲染要经过以下过程:

  1. 解析html,生成dom树;解析css,生成样式规则树。
  2. 将dom树和样式规则树结合,生成渲染树(render tree).
  3. 根据生成的渲染树,确定元素的布局信息(元素的尺寸和位置),这一步成为reflow,叫重拍或回流。
  4. 根据渲染树和布局信息,生成元素的像素信息(元素横纵像素点,左上角的偏移量,每个像素的颜色等),这一步称之为repaint,叫重绘。
  5. 将像素信息提交到GPU完成屏幕绘制。

当元素的布局信息发生变化时,会导致重排。当元素的像素信息发生变化时,会导致重绘。重排一定会引起重绘。因为布局信息变化一定会导致像素信息的变化。

获取和设置元素尺寸、位置均会导致重排和重绘。 设置元素外观,如背景色,只会导致重绘。

重排是一项繁琐的工作,会降低效率,因此开发者,尽量避免直接获取和设置元素的尺寸和位置。尽量使用变量保存元素的布局信息。

DOM树的形成

我们编写的html、css和js代码,在浏览器中都是0和1,字节数据。 当浏览器接收到字节数据后,他会将字节数据转化为字符串,就是我们的可以看到的代码。 当转为字符串以后,浏览器会先将这些字符串进行词法分析转换为标记(token)。这一过程在词法分析中叫做标记化(tokenization)。

标记化就是拆分为一个个短字符串,拆分为最小单元

当标记化结束后,这些标记会紧接着转换为DOM节点,之后所有DOM节点会根据彼此之间的关系,形成一棵DOM树。

CSSOM树的形成

接下来是CSS转换为CSSOM的过程。 字节数据-->字符串-->字符串标记-->Node-->CSSOM 在这一过程中,会确定每一个节点/元素的样式,并生成一颗样式规则树,这棵树记录的是每一个DOM节点的样式。

css转换为cssOM这一过程是很消耗资源的。因为浏览器需要递归cssom树,然后确定具体的元素到底是什么样式。

所以,我们应该尽量少的添加无意义的标签,也避免过于具体的css元素器,减少div>span>a这样的选择器,保证层级扁平。

生成渲染树

生成DOM树和CSSOM后,就需要将两棵树合并为渲染树(Render Tree). display:none的节点不会在渲染树中。 当浏览器生成渲染树以后,就会进行布局(回流),之后确定每一个像素点的信息(重绘)。然后调用GPU绘制,合成图层,显示在屏幕。

阻塞和渲染

渲染的前提是生成了渲染树,而生成渲染树的前提是生成了DOM树和CSSOM样式规则树。 所以想要渲染的速度加快,就要降低渲染文件的大小,并且html阶段层级扁平华,优化选择器。

当浏览器解析到script标签时,会暂停绘制DOM树。因为js引擎线程和GUI渲染线程时互斥的。根本原因时js可以修改DOM节点,所以浏览器先执行js代码,然后再从暂停的地方接着绘制。

所以js代码都要放在body标签的底部。

如何避免js代码阻塞渲染:

  • async
  • defer
  • prefetch
  • preload

重绘和回流

回流:元素的尺寸和位置等几何属性变化。 重绘:外观更改而不影响布局,比如背景色、字体颜色等属性。

回流必定会重绘。重绘不一定引起重排。所以回流的成本比重回高。 改变子节点很可能会导致父节点的回流。

回流的情况:

  • 添加或删除可见的DOM元素
  • 元素的位置变化
  • 元素的尺寸变化,外边距,内边距,边框大小
  • 内容发生变化,比如文本变化,或图片被另一不同尺寸的图片替代
  • 页面初次渲染时
  • 浏览器窗口尺寸变化,因为重排是根据适口大小来计算元素的位置和大小的

现代浏览器的优化机制

由于每次重拍都会造成额外的计算消耗,因此大多数浏览器通过队列化修改并批量执行来优化重排过程。

浏览器会将修改操作放入队列,当到达一定时间或操作达到了一个阈值,才会清空队列。 当获取布局信息时,会强制队列刷新:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect

减少回流和重绘的方式

  • 最小化重绘和回流
const div = document.getElementById("#id");
div.style.cssText += 'border-left:1px silid red;border-right:1px solid green; padding:5px;'

// 将要添加的样式写到一个样式类里,通过添加和删除样式类的方式改变样式
div.className += ' active';
  • 批量修改DOM
  1. 使元素脱离文档流
  2. 进行多次修改
  3. 带回文档中
    • 隐藏元素,应用修改,重新显示
    • 使用文档片段在当前dom之外构建一个子树,再拷贝回文档
    • 将原始元素拷贝到一个脱离文档流的节点中,修改后,再替换原始的元素
function appendDataToEle(ele,data){
    let li;
    for(let i=0;i<data.length;i++){
        li = document.createElement('li');
        li.textContent = 'text';
        ele.appendChild(li);
    }
}
const ul = document.getElementById("id");
ul.style.display = 'none';
appendDataToEle(ul,data);
function appendDataToEle(ele,data){
     let li;
     for(let i=0;i<data.length;i++){
         li = document.createElement('li');
         li.textContent = 'text';
         ele.appendChild(li);
     }
}
const ul = document.getElementById("id");
const fragment = document.createDocumentFragment();
appendDataToEle(fragment,data);
ul.appendChild(fragment);
function appendDataToEle(ele,data){
    let li;
    for(let i=0;i<data.length;i++){
        li = document.createElement('li');
        li.textContent = 'text';
        ele.appendChild(li);
    }
const ul = document.getElementById("id");
const cloneUl = ul.cloneNode(true);// 深度克隆
appendDataToEle(clone,data);
ul.parentNode.replaceChild(clone,ul);
  • 避免触发同步布局事件
const width = box.offsetWidth;
function(){
    for(let i=0;i<pList.length;i++){
        // pList[i].style.width = box.offsetWidth + 'px';
        pList[i].style.width = width + 'px';
    }
}
  • 复杂动画脱离文档流 使用css3硬件加速,可以让transform、opacity、filters、will-change于background-color这些还是会引起回流重绘的,不过还是可以提升动画的性能。

将复杂动画,使用绝对定位脱离文档流

  • css3硬件加速-GPU加速

常见面试题

为什么操作dom慢? dom属于GUI渲染线程,js属于js引擎线程。通过js操作dom就会引起两个线程直接的通信。从而产生性能消耗。另外操作dom也会引起重绘和重排,也有性能上的消耗。 频繁的dom操作,会造成卡顿,影响用户体验。 所以react、vue等框架提出来虚拟dom的概念。就是为了解决浏览器性能提出的。