一文让你彻底搞懂浏览器的渲染流程

1,798 阅读9分钟

hello,大家好。上次为大家介绍了重排和重绘的一些内容,其中涉及到的浏览器渲染流程自己就没有在上篇博文详细介绍了。今天我们就来好好唠唠浏览器的整个渲染流程。

首先,我们知道,一个页面通常由三个部分组成:js,css,html。浏览器拿到这三个文件之后,通过一系列步骤将其渲染到页面上呈现给用户,下面我们就来详细的解释整个流程。

DOM树构建

从你输入网址到拿到服务器返回的数据后,进行的第一个步骤就是将 HTML 解析成DOM 树。为什么要有这一步?因为浏览器是无法识别 HTML 文件的内容的,需要把他转化成浏览器能够理解的 DOM 树。相当于你和一个英国人交流,但是他不懂中文,需要把中文翻译成英文这个英国人才能理解。在这里 HTML 文件就是中文,DOM树就是英文。

在 chrome 的控制台中输入 document ,我们就能看到什么是 DOM 树

在这里插入图片描述
它是存储在内存中的树状结构,能够被 JS 直接操控。

CSSOM 树的构建

CSS 的来源主要有三个

  • 外部样式表
  • 内部样式表
  • 内联样式

在这里插入图片描述
同样的,浏览器在拿到 css 文件之后,也需要将转变成浏览器能够理解的形式:styleSheets,它包含了从三个地方取过来的全部样式。

在 chrom 中,我们可以通过 document.styleSheets 来查看该结构

在这里插入图片描述
随后浏览器会根据 CSS 的继承规则和层叠规则,将其运用到 DOM 节点中,得到一颗 CSSOM 树,包含了各种节点的样式。

生成渲染树

有了 DOM Tree 和 CSSOM 之后,我们还需要将两者结合生成一颗渲染树。渲染树和这两者之间有什么区别呢?我们用一张图来看看这个过程

在这里插入图片描述
可以看到,DOM TREE 和 CSSOM 中存在很多不可见的元素,比如 head 标签和display 值为 none 的 p 标签,在生成 RENDER TREE之后,里面只会保留可见的元素而剔除掉不可见元素。

生成图层树

在生成 RENDER TREE之后,浏览器并不能直接进行绘制,这是为什么呢?熟悉前端的同学肯定知道,在前端开发中有一个层叠上下文的概念。所以浏览器在渲染时同样需要分层,最终将许多图层叠加在一起,就形成了最终呈现给我们的页面。那么,浏览器会如何进行分层?

首先,拥有层叠上下文的元素会被提升为单独的一层,其被称之为渲染层,其主要目的是为了实现层叠上下文。浏览器后期就会根据渲染层由内到外依次叠加,最终呈现给用户。

之所以把渲染层单独指出来,因为其实还存在一种合成层。这个就和我们在重排重绘那篇文章中提到过的如何直接跳到合成阶段的内容有关,也是我们平时说的 CSS3硬件加速。更具体的内容可以查看淘系前端团队分享的文章 无线性能优化:Composite

绘制列表生成

现在,浏览器终于可以开始干正事了。它会将各个图层的绘制分成很多个小的绘制指令并存入到一个列表中。在 chrome 中我们可以在 Layers 点击某个合成层,随后再点击 Paint Profiler 来查看绘制列表。

在这里插入图片描述
就像下面这样
在这里插入图片描述

光栅化

Rasterization,光栅化,又称为栅格化,它用于执行绘图指令生成像素的颜色值。 实际上浏览器不是直接对整个图层进行光栅化,它会将图层分块,然后以块为单位进行光栅化。

合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

至此,浏览器的整个渲染的大流程就已经讲解完毕。我们来总结一下这几个步骤:

  • 生成 DOM TREE
  • 生成 CSSOM
  • 生成 RENDER TREE
  • 生成图层树,其中包括渲染层和合成层
  • 绘制列表生成
  • 分块光栅化
  • 合成和显示

在了解了整个流程之后,我们再来通过几个常见的小问题加深一下对于浏览器渲染过程的理解。

为什么 JS 文件建议放在 body 的最后而 CSS 文件建议放在前面?

这个问题我相信很多人都知道如何去回答:因为 JS 会阻塞 DOM 树的解析,所以要放在最后,而 CSS 文件要尽早加载,所以要放在最前面。

不错,就是这样的,但是我们结合刚刚所说的浏览器渲染流程:

DOM TREE -> CSSOM -> RENDER TREE

既然DOM要解析完才能生成最后的 RENDER TREE,那么放在开头和结尾有区别吗?

首先问是不是,再来问为什么。RENDER TREE真的是要等 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>
  <!-- <link rel="stylesheet" href="http://127.0.0.1:5500/big.css"> -->
</head>
<body>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <script src="./index.js"></script>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
</body>
</html>
//index.js
console.log("hello world");

我们起一个本地服务器后,将 chrome 中 network 的 online 选项调成 Fast 3G

在这里插入图片描述
然后再刷新页面
在这里插入图片描述
我们看到 script 的上半部分内容首先显示出来,后半部分内容随后才加载出来。我们再用 performence 分析一下
在这里插入图片描述
DOM 树构建被 JS 打断之后就已经出现了 Composite Layers 阶段,即已经有内容渲染到了界面上。同时 First Paint 实际发生在 645ms。

而 JS 文件加载完之后,浏览器会继续解析剩下的内容,同时随后会触发 DOMContentLoaded 事件

在这里插入图片描述
下面我们把 JS 的加载的代码提前到 head 标签中来。
在这里插入图片描述
可以看到,浏览器需要等 JS 文件下载完之后才开始真正的渲染,同时 First Paint 事件延后到了 1250ms 左右。

看到这我相信大家都明白了,JS 虽然会阻塞 DOM 树的构建,但是已经解析完成的 DOM 树能够提前被浏览器渲染出来呈现给用户,能够减少白屏时间,将 First Paint 的时间提前。

但是对于 DOMContentLoaded 事件而言,JS 无论放在哪里,对其都没有影响,因为要 DOM 解析完毕之后才会触发 DOMContentLoaded 事件。

而为什么 CSS 最好放在开头,我们下个问题解释。

CSS文件会阻塞渲染吗?

在 chrome 的官方文档中,明确的指出了 CSS 文件会阻塞浏览器的渲染。但是,CSS 的加载,通常情况下并不会阻塞 DOM 树的解析。为什么说通常情况下,因为文档中也提到了,在 JS 加载的时候,如果 CSSOM 没有生成,浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。为什么要这样做?因为 JS 也有操纵 CSS 的能力,所以在 CSSOM 没有构建完成之前,我们也不能运行 JS 文件,否则很可能会做许多重复的工作,对性能不利。

我们做一个简单的 demo 来验证一下。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./big.css">
</head>
<body>
  <div class="demo" id="hello">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <div class="demo">上半部分</div>
  <script src="./index.js"></script>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
  <div class="demo">下半部分</div>
</body>
</html>

首先还是将 online 调成 Fast 3G。同时写一个非常大的 CSS 文件,这个我就不展示给大家看了,因为行数非常多,大家自己造一个就行了。利用 performance 工具我们可以看到,虽然 JS 文件已经加载完毕,但是仍然没有继续渲染剩下的 DOM

在这里插入图片描述
为什么呢?因为 JS 需要 CSSOM 构建完毕之后才能执行,但是 JS 是会阻塞 DOM 树的解析的,所以 CSS 文件也就间接的阻塞了 DOM 树的解析。

所以,CSS 文件肯定会阻塞浏览器的渲染,但是不一定会阻塞 DOM 树的解析。

同时,我们来回最开始的问题,为什么 CSS 文件放在开头比较好?因为 CSS 文件越早开始下载,就能越早构建出 CSSOM 树,才能更快的渲染出内容来。

结语

这就是今天这篇博客的全部内容了,希望各位看观动动小手给个赞和关注~有错误的地方也可在评论区指出大家共同探讨。