浏览器系列 -- 渲染原理及过程

795 阅读11分钟

从输入 URL 到页面加载的全过程参考 计算机网络系列 -- 从URL到网页加载

这里主要讲解页面渲染部分的过程

渲染流程概述

浏览器从最初接收请求来的HTMLCSS、javascript等`二进制形式`的资源,然后`解析、构建树、渲染布局、绘制`
最后呈现给用户能看到的界面的整个过程

渲染引擎

一个渲染引擎主要包括:HTML解析器,CSS解析器,javascript引擎,布局layout模块,绘图模块

  • HTML解析器:解释HTML文档的解析器,主要作用是将HTML文本解释成DOM树。
  • CSS解析器:它的作用是为DOM中的各个元素对象计算出样式信息,为布局提供基础设施
  • Javascript引擎:使用Javascript代码可以修改网页的内容,也能修改css的信息,javascript引擎能够解释javascript代码,并通过DOM接口和CSS树接口来修改网页内容和样式信息,从而改变渲染的结果。
  • 布局(layout):在DOM创建之后,Webkit需要将其中的元素对象同样式信息结合起来,计算他们的大小位置等布局信息,形成一个能表达这所有信息的内部表示模型
  • 绘图模块(paint):使用图形库将布局计算后的各个网页的节点绘制成图像结果

渲染整体过程

此段内容参考 深入剖析:Vue核心之虚拟DOM

  1. 构建 DOM 树:用 HTML 分析器,解析 HTML 元素,构建一棵 DOM 树;
  2. 构建 CSSOM 树:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,构建出一棵 CSSOM 树;
  3. 若在构建 DOM 树的过程中,当 HTML 解析器遇到一个 script 标记时,即遇到了 JS,将立即阻塞 DOM 树的构建,将控制权移交给 JS 引擎,等到 JS 引擎运行完毕,浏览器才会从中断的地方恢复 DOM 树的构建;

其根本原因在于,JS会对 DOM 节点进行操作,浏览器无法预测未来的 DOM 节点的会被 JS 怎么样去修改,所以为了防止无效操作节省资源,只能阻塞DOM树的构建。例如:若 JS 删除了某个 DOM 节点 A,那么浏览器为构建此节点 A 花费的资源是无效的

  1. 构建 Render 树:将 DOM 树和 CSSOM 树关联起来,构建一棵 Render 树;

每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象,这些 render 对象最终会被构建成一棵 Render 树;

  1. 确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点计算确定一个在显示屏上出现的精确坐标;
  2. 绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。

注意事项

  1. 在一个 HTML 文档里面,浏览器渲染引擎会看标签办事
1. 当这些节点是`普通节点`的话,HTML解析器就会将这些节点`加入到DOM树中`
2. 当这些节点是`JS代码`的话,HTML解析器就会将控制权`交给JS引擎`
3. 如果这些节点是`CSS代码`的话,HTML解析器就会将控制权`交给CSS解析器`
  1. 为了尽可能避免 JS 阻塞渲染,我们通常采取以下两种措施:
  • 将 JS 文件放到 HTML 底部进行加载
  • 对 JS 文件执行 asyncdefer 加载。
  1. 上面的步骤中的每一步(除了第 3 步的 JS 阻塞渲染外)都不是说必须得上一步完成后才能继续往下一步走,就是说浏览器会一边解析 HTML、一边解析 CSS 、一边构建 render 树、一边进行计算和绘制。都是为了尽快将内容显示在屏幕上,以达到更好的用户体验
  2. 浏览器甚至还会在没有完全接收 HTML 文件时便开始渲染、显示网页,即边接收边渲染;在执行 HTML 中代码时,根据需要,浏览器会继续请求图片、CSS、JavsScript等文件,过程同请求 HTML

渲染具体过程

1. 构建DOM树

当浏览器接收到服务器响应来的HTML文档后,会遍历文档节点,HTML解析器开始工作,构建DOM树

image.png

浏览器接收到服务器响应来的HTML文档是二进制字节流的形式

image.png

过程分析
  1. Bytes:当服务器返回一个HTML文件给浏览器时,浏览器接受到的是一些字节数据
  2. Characters:浏览器根据HTTP响应中的编码方式(通常是utf-8),解析字节数据,得到HTML文档
  3. Tokens:浏览器根据DTD中的对元素的定义,对接受到的字符进行语义化(tokens)
  4. Nodes:浏览器使用这些语义块(tokens)创建对象(Nodes)
  5. DOM:HTML解析器构建当前节点的所有子节点,再去构建当前节点的下一个兄弟节点
1.由于CSS解析器、JS解析器的优先级大,所以DOM树会暂时停止构建,因而造成`白屏`,就是所谓`浏览器阻塞`
2.不过,当外联的JS代码和CSS代码还没从服务器传到浏览器的时候,如果此时`DOM树上有可视元素`的话
浏览器通常会将一些内容`提前渲染`到屏幕上来

阻塞渲染问题参考 浏览器系列 -- 阻塞渲染

  1. 当HTML解析器读到最后一个节点的时候,整个DOM树也构建完成了,这个时候就会触发【domContentloaded】事件。至此,DOM树就全部构建完成了。
1. `domContentloaded`事件触发时:仅当DOM加载完成,`不包括样式表,图片等`
2. `load`事件触发时:页面上`所有的DOM、样式表、脚本、图片都已加载完成`
注意
1. DOM树在构建的过程中可能会因为`CSS和JS的加载``被阻塞`
2. `display:none`的元素也会在DOM树中
3. `注释`也会在DOM树中
4. `script标签`会在DOM树中

2. 构建CSSOM规则树

当获取style标签内的css、或者内嵌的css,浏览器会发送请求获得该标签中标记的CSS

当渲染引擎接收到 CSS 二进制文本后,会经过一系列的转换,最终变成浏览器可以理解的styleSheets

image.png

CSSOM规则树的构建也是要经过 Bytes→characters→tokens→nodes→object model 的过程

CSS 解析是自右至左读取的,所以 右边 的选择器的属性会覆盖 左边 的选择器的属性,同时我们可以得知:嵌套太多层会影响性能

注意
1. CSS解析可以与DOM解析`同时进行`
2. CSS解析与script的`执行互斥`
3.Webkit内核中进行了script执行优化,`只有在JS访问CSS时`才会发生互斥
分析
2. CSSOM规则未构建好`会阻塞`渲染树的构建(即CSS阻塞渲染)
3. JS代码的执行会阻塞DOM解析,而如果JS代码访问了CSS,则CSS解析优先于JS代码优先于DOM树的构建
(联系到重绘、回流和浏览器阻塞渲染)

阻塞渲染问题参考 浏览器系列 -- 阻塞渲染

3. 构建渲染树

浏览器会先从DOM树的根节点开始遍历每个可见节点,然后对每个可见节点找到适配的CSS样式规则并应用

image.png

注意:渲染树和DOM 树【不完全对应】
因为
1. 【display: none】【meta】【script】【link】的元素【不在】Render Tree中
2. 但【visibility: hidden】的元素【在】Render Tree中

渲染树生成后,还是没有办法渲染到屏幕上,渲染到屏幕需要得到各个节点的位置信息,这就需要布局(Layout)的处理了

4. 渲染树布局(layout of the render tree)

根据生成的渲染树,从渲染树的根节点开始遍历,计算每个节点的几何信息(在设备视口内的确切位置大小)

布局阶段的输出就是常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小

注意
1.float元素】,【absoulte元素】,【fixed元素】会发生位置偏移。
2. 【脱离文档流】,其实就是【脱离Render Tree】

5. 渲染树绘制(Painting the render tree)

经过生成的渲染树和Layout阶段,得到了所有可见节点具体的几何信息与样式,然后将渲染树的每个节点转换成【屏幕上的实际像素】。渲染树的绘制工作是【由浏览器的UI后端组件】完成的

重绘与回流

我们都知道HTML默认是流式布局的,但CSS和JS会打破这种布局,尤其是用户与网页交互时,【改变DOM的外观样式以及大小和位置】。因此我们就需要知道两个概念:repaintreflow

重绘 repaint (操作一个DOM节点不会影响其他节点)

在布局过程中,遇到style属性或js操作,改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,被改变元素本身要重画,但是元素的几何尺寸、元素以及其他元素的位置没有发生改变

回流 reflow (操作一个DOM节点会影响其他节点)

布局过程中,遇到style属性或js操作,对元素的大小、定位等有改变,需要重新计算和绘制所有的结点几何尺寸和位置,则会引起回流。

值得注意的是:比如修改了元素的样式,浏览器【并不会立刻】reflow或repaint一次
因为有【flush队列】让多次回流重绘【变成一次】回流重绘

触发重绘的原因

除了元素的尺寸和位置变化的变化:

背景色、边框颜色、文字颜色、字体改变

触发回流的原因

  1. 页面第一次渲染(初始化)
  2. DOM树变化(如:增删节点)
  3. padding、margin改变
  4. 元素的尺寸大小、包括文字大小改变
  5. 浏览器窗口resize
  6. 获取元素的某些属性

减少reflow、repaint触发次数

CSS方面 —— (多半是style属性引起的:把样式放在style标签或link标签)

  1. 避免设置多层内联样式,将样式合并在一个外部类
// 内联样式
<div style="width: 65px;height: 20px;border: 1px solid;">元素</div>
// 外部类
<head>
    <meta charset="utf-8" />
    <style type="text/css">
        div {
            width: 65px;
            height: 20px;
            border: 1px solid;
        }
    </style>
</head>
  1. 将需要多次回流的元素position属性设为absolutefixed(比如动画效果)
position属性设为absolute或fixed意味着脱离文档流,因而元素样式发生改变时不会引起回流重绘

JS方面 —— (操作DOM节点时引起的:重点减少次数)

  1. 避免多次读取 DOM 节点可以用变量将结果存起来
  2. 需要多次操作同一 DOM 节点时可以先将该节点的元素设置display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘)
  3. 操作多个 DOM 节点时将所有的 DOM 操作集中在一起(原因如下)
a. 浏览器会维护1`队列`,把所有会引起回流、重绘的操作放入这个队列
b. 等队列中的操作到了`一定的数量`或者到了`一定的时间间隔`,浏览器就会`flush队列`,进行一个批处理
c. 这样就会让多次的回流、重绘`变成一次`回流重绘
d. 有特殊情况,会`提前执行`flush队列
需要获取这些值时:
width、height;
clientWidth、clientHeight、clientTop、clientLeft;(clientWidth = width+左右padding)
offsetWidth、offsetHeight、offsetTop、offsetLeft;(offsetWidth = width+左右padding+左右boder)
scrollWidth、scrollHeight、scrollTop、scrollLeft;

4.形变和位移用transfrom代替 DOM 节点操作

附加问题

display:none和visibility: hidden的区别

相同点:控制元素的显示与隐藏

不同点:

  1. 意义上:display:none表示元素不要了,visibility: hidden表示元素看不见
  2. 原理上:display:none的元素不在渲染树上,visibility: hidden的元素在渲染树上
注意:display:none的元素虽然不在渲染树上,但会在 DOM 树上
  1. 影响上:设置元素为display:none时会引起回流,设置元素为visibility: hidden会引起重绘
注意:display:none的元素是操作 DOM 节点,使得该元素所在节点去掉,所以需要回流

回流 reflow 和重绘 repaint 的联系与区别

联系:回流 reflow 必定引起重绘 repaint

区别:

  1. 本质上
  2. 触发原因上
  3. 过程上

如何减少reflow、repaint触发次数

CSS 方面
  1. 外部类代替多个内联样式
  2. 设置position脱离文档流
JS 方面
  1. 多次读取 DOM 节点时变量来存
  2. 多次操作同一 DOM 节点时先设置display:none
  3. 同时操作多个 DOM 节点时集中,有flush队列
  4. 形变和位移用transfrom代替 DOM 节点操作

如何优化渲染效率

优化方案见 浏览器系列 -- 优化页面加载