面试题之一文搞定浏览器的渲染原理

浏览器渲染原理:

从服务器获取的HTML字符串渲染到页面的整体过程包括以下几步:

  • 解析 HTML

  • 样式计算

  • 布局

  • 分层

  • 生成绘制指令

  • 分块

  • 光栅化

  • 绘制

解析 HTML:

整体过程:解析 html代码,生成 DOM 和CSSOM树

在解析的过程中,会遇到link、script标签,对于这类标签,浏览器有特殊的操作:

为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML中的外部 CSS 文件和外部的 JS文件。

那么会有一种情况,那就是渲染主线程在解析的过程中,解析到了link/script标签,而此时外部 CSS 文件/外部的 JS文件还没下载解析好(下载和解析是两个过程),那么对于css文件和js文件的处理又是不同的情况了

对于CSS 文件:

如果外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作都不是在渲染主线程中进行的,这也是 CSS 不会阻塞 HTML 解析的根本原因(HTML 解析是在渲染主线程上进行的)

补充: 虽然下载解析css外部文件不会阻塞 HTML 解析,但css解析完成生成CSSOM树时,会阻塞 HTML 解析,如下图

image-20240712163002966

对于js文件:

一般情况下:

<script src="script1.js"></script>

如果外部的 js没有下载解析好,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。

image-20240712163755472

补充: 如果你想首屏渲染的越快,就越不应该在最前面就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因

async情况:

<script async src="script1.js"></script>

与一般情况不同的是,当标签加了sync后,js的下载过程不会阻塞 HTML的解析,在js下载的过程中,渲染主线程继续解析HTML,直到js下载完成后,才会停止HTML解析,转而解析执行js

补充: 多个带有async属性的script标签之间的执行顺序是不确定的,也就是说,谁先下载完,谁先执行。因此,如果一个脚本依赖于另一个脚本加载完成后才能执行,就不能使用async属性

image-20240712164705041

defer情况:

<script defer src="script1.js"></script>

在下载方面,defer情况与async相同,但下载完成后,js不会立即解析执行,会等待HTML解析完成后再执行js

补充: 多个带有defer属性的脚本标签之间的执行顺序是确定的,即按照它们在文档中出现的顺序依次执行

image-20240712164727705

除了async和defer以外,浏览器中还为我们提供了新的方式来避免 JS 代码阻塞渲染的情况:preload和prefetch

preload:

<link rel="preload" href="script.js" as="script">

浏览器会在遇到如下link标签时,立刻开始下载js(不阻塞HTML解析),并放在内存中,但不会执行其中的JS语句。 只有当遇到link对应的script标签加载的时候,浏览器才会直接将预先加载的JS执行。

prefetch:

<link rel="prefetch" href="script.js" as="script">

浏览器会在空闲的时候,下载文件并缓存。当有页面使用的时候,直接从缓存中读取。其实就是把决定在什么时间加载这个资源的决定权交给浏览器。 如果prefetch还没下载完之前,浏览器发现script标签也引用了同样的资源,浏览器会再次发起请求,这样会严重影响性能的,加载了两次,所以不要在当前页面马上就要用的资源上用prefetch,要用preload

preload 和 prefetch 的区别:

  • preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源

  • prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源

  • 在VUE SSR生成的页面中,首页的资源均使用preload,而路由对应的资源,则使用prefetch

  • 对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch

样式计算:

整体过程:经过样式计算后,之前的 DOM 树和 CSSOM 树合并成了一颗带有样式的 DOM

样式计算:元素从所有css属性都没有值到所有css属性都有值的过程,在这一过程中,很多预设值会变成绝对值,比如 red 会变成 rgb(255,0,0)。相对单位会变成绝对单位,比如 em 会变成 px

样式计算分为四步:优先级由上到下依次降低

  • 确定声明值

  • 层叠冲突

  • 继承

  • 使用默认值

确定声明值:

在浏览器样式表以及作者样式表中(即自己写的样式)没有发生冲突的样式直接作为计算后的样式结果

接下来处理有冲突的属性

层叠冲突:

冲突:即同一样式多次应用到同一个元素

对样式表有冲突的声明使用层叠规则,确定css属性值

规则:

  • 比较重要性

重要性从高到低:

1.作者样式表中的!important样式

2.作者样式表中的样式

3.浏览器默认样式表的样式

  • 比较特殊性

看选择器,权重计算

千位:内联样式

百位:id选择器数量

十位:类选择器,属性选择器,伪类选择器数量

个位:标签选择器,伪元素选择器数量

  • 比较源次序

css代码中下面的覆盖上面的

经过上述规则后,仍然会存在无法确认的属性,如作者样式表和浏览器默认样式表中都没有声明的属性,就需要通过下面两步来确定了

继承:

对于仍然没有值的属性,若能继承,则继承父元素身上的属性

一般关于文本、文字的样式是可以继承的,如color,font-size等等

但仍然存在一些不能继承的属性,如背景,定位等等,这时候就通过最后一步来确认了

使用默认值:

每个属性都有一个默认值,如果前面的步骤都无法确定下来,那么就只能是使用默认值作为最终的计算结果

经过样式计算后,就得到一棵带有样式的 DOM

布局:

主线程会遍历刚刚构建的 DOM 树,根据 DOM 节点的计算样式计算出一个布局树。

最终确定下来的布局树与前面生成的dom树一般情况下并非是一一对应的

比如 display:none 的节点没有几何信息,因此不会生成到布局树:

image-20240712190157285

比如使用了伪元素选择器(::before,::after),虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中

image-20240712190251295

还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应

image-20240712190314736

补充:

reflow,又叫重排,其实就是重新计算布局树的过程(进行了影响布局树的操作,例如修改元素的宽高),又因为布局树的改变,导致后面的步骤都需要重新进行一次

为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。浏览器在反复权衡下,最终决定获取属性立即 reflow

值得注意的是,像浮动、绝对定位等脱离文档流的属性的修改并不会引起重排

分层:

为了确定哪些元素需要放置在哪一层,主线程需要遍历整颗布局树来创建一棵层次树

分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。

滚动条、堆叠上下文、transformopacity 等样式都会或多或少的影响分层结果,也可以通过使用 will-change 属性来告诉浏览器对其分层

关于will-change的作用可以参考这篇文章:

使用will-change来提高页面的渲染速度

生成绘制指令:

分层工作结束后,接下来就是生成绘制指令。

主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。生成绘制指令集后,渲染主线程的工程就暂时告一段落,接下来主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成

image-20240712191037875

补充:

repaint,又叫重绘,其实就是重新根据分层信息计算了绘制指令。当改动了可见样式(如color)后,就需要重新计算,会引发 repaint。

由于元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint,但反之不成立

分块:

合成线程会对每个图层进行分块,将其划分为更多的小区域。合成线程不再是像主线程那样一个人在工作,它会从线程池中拿取多个线程来完成分块工作,即分块是由多个线程同时进行的

光栅化:

将每个块变成位图,即确认每个像素点的rgb信息

光栅化的操作,并不由合成线程来做,而是会由合成线程将块信息交给 GPU 进程,以极高的速度完成光栅化。GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块

绘制:

当所有的图块都被栅格化后,合成线程会拿到每个层、每个块的位图,从而生成一个个指引信息

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形

变形发生在合成线程,与渲染主线程无关,这就是 transform 效率高的本质原因。

整体来说,这是一个非常复杂过程,其中涉及到的许多知识点可以用来解释我们在开发中遇到的一些理所当然的现象,比如为什么a标签不能继承父元素的color属性,为什么推荐使用transform而不是使用定位来实现元素偏移等等。当然,也是一道很好的面试题,我在应答“输入url到加载完页面发生了什么”这个问题时,其中就涉及到了浏览器的渲染原理。

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值