文章目录
构建DOM树
样式计算
布局阶段
分层
绘制
分块
光栅化
合成
- 应用层(Layer 7)
浏览器解析 URL:浏览器解析用户输入的 URL(例如 http://example.com)并确定要请求的资源。
HTTP 请求:浏览器构建一个 HTTP 请求消息,包括请求方法(如 GET、POST)、头部信息和请求体(如果有)。 - 表示层(Layer 6)
数据编码:HTTP 请求消息被编码成字节流,表示层负责将数据转换为合适的格式以便在网络上发送。对于 HTTP,通常是将字符编码为 UTF-8 或其他编码格式。 - 会话层(Layer 5)
会话管理:会话层负责管理和维持会话。对于 HTTP 请求,主要是管理连接的建立和关闭,特别是在使用 HTTPS 时,涉及到安全连接的建立。 - 传输层(Layer 4)
TCP/UDP 连接:在传输层,浏览器使用 TCP(传输控制协议)建立与服务器的连接。TCP 负责数据包的分段、排序和重组,确保数据的可靠传输。
端口号:传输层还负责分配端口号,例如 HTTP 使用端口 80,HTTPS 使用端口 443。 - 网络层(Layer 3)
IP 数据包:在网络层,数据被封装成 IP 数据包,包含源 IP 地址和目标 IP 地址。路由器在这个层次进行数据包的转发和路由选择。
IP 地址解析:如果浏览器没有缓存目标服务器的 IP 地址,它会使用 DNS 进行域名解析,将域名转换为 IP 地址。 - 数据链路层(Layer 2)
帧封装:在数据链路层,IP 数据包被封装成帧,这些帧通过物理网络进行传输。数据链路层负责局部网络中的数据传输和错误检测。
MAC 地址:数据链路层处理以太网帧或其他协议帧,包括源 MAC 地址和目标 MAC 地址的处理。 - 物理层(Layer 1)
实际传输:在物理层,数据被转换为电信号、光信号或无线信号,通过物理介质(如电缆、光纤或无线电波)传输到目标设备。
网络层
SNAT、DNAT
-
网络地址转换技术(SNAT 和 DNAT),SNAT 和 DNAT 主要发生在网络层(通常在路由器、NAT 网关等设备上)
-
SNAT(源地址转换)
- 当浏览器发出请求时,如果用户的网络在使用私有 IP 地址(如 192.168.x.x),则需要通过 NAT 网关将私有 IP 地址转换为公共 IP 地址。此过程称为 SNAT
- 例如,浏览器发出的请求源地址为 192.168.1.2,通过 NAT 网关转换为公共 IP 地址如 203.0.113.1,然后发送到互联网
-
DNAT(目标地址转换)
-
当请求到达百度服务器的边界设备(如负载均衡器、防火墙等)时,可能会将公共 IP 地址转换为内部服务器的私有 IP 地址,以便进行负载均衡或安全管理。此过程称为 DNAT
- 例如,请求目标地址为 123.456.78.90,通过 DNAT 转换为内部服务器的地址如 10.0.0.1
-
NAT 作用
- IPv4 地址空间(公网 IP)有限,无法为每个互联网设备分配一个唯一的公共 IP 地址。通过 NAT,可以多个设备共享一个公共 IP 地址,从而缓解 IP 地址短缺的问题
- 使用私有 IP 地址的设备无法直接从互联网访问,这增加了本地网络的安全性。NAT 充当防火墙的一部分,保护本地网络免受外部攻击
构建DOM树
- 字节→字符→令牌→节点→对象模型(DOM)

样式计算
- 格式化样式表
- 渲染引擎收到CSS文本数据后,会执行一个操作,转换为浏览器可以理解的结构-styleSheets,通过浏览器的控制台document.styleSheets可以来查看这个最终结果

- 标准化样式表

- 计算每个DOM节点具体样式
- 在计算完样式之后,所有的样式值会被挂在到window.getComputedStyle当中
- 最终拼接为一个树状的 CSSOM
- 之所以是将 css 也处理也树状结构,因为 css 的规则是支持“向下级联”的嵌套方案的,也就是日常开发中 css 的继承特性
- 浏览器在计算任何节点的样式时,它会从适用于该节点的最通用(顶层)的规则开始进行计算。比如,如果需要计算的节点是 body 元素的子元素,那么它会应用 body 的样式,之后会一层一层进行递归该过程从而得到该节点最终的样式。
- 之所以是将 css 也处理也树状结构,因为 css 的规则是支持“向下级联”的嵌套方案的,也就是日常开发中 css 的继承特性
- 渲染引擎收到CSS文本数据后,会执行一个操作,转换为浏览器可以理解的结构-styleSheets,通过浏览器的控制台document.styleSheets可以来查看这个最终结果
生成布局树
- 在DOM树上不可见的元素,head元素,meta元素等,以及使用display:none属性的元素,最后都不会出现在布局树上,所以浏览器布局系统需要额外去构建一棵只包含可见元素布局树
-
从 DomTree 开始遍历,遍历每一个可见节点
- 一些脚本标签、元标签等节点是不可见的,由于它们未反映在页面的呈现中所以会被被省略。
- 同时对于一些通过 CSS 隐藏的节点,也会从渲染树中省略。比如,上述 HTML 中的 span 节点在上面的例子中会在渲染树中丢失,因为它明确的设置了 “display: none” 属性。
-
对于 DomTree 中的每个可见节点,在 Cssom 中找到合适的匹配 CSSOM 规则并应用它们。
-
最终在 Render Tree 上挂载这些带有内容以及样式的可见节点。
-

分层
- 为了方便处理 Positioning(定位),Clipping(裁剪),Overflow-scroll(页內滚动),CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z-indexing(Z排序)等,浏览器需要生成另外一棵树 - Layer 树
- 渲染引擎会为一些特定的 RenderObject 生成对应的 RenderLayer,而这些特定的 RenderObject 跟对应的 RenderLayer 就是直属的关系,相应的,它们的子节点如果没有对应的 RenderLayer,就从属于父节点的 RenderLayer。最终,每一个 RenderObject 都会直接或者间接地从属于一个 RenderLayer。
- 一些 RenderLayer 会拥有自己独立的缓存,它们被称为合成图层(Compositing Layer)
- WebKit 会为这些 RenderLayer 创建对应的 GraphicsLayer,不同的浏览器需要提供自己的 GraphicsLayer 实现用于管理缓存的分配,释放,更新等等。拥有 GraphicsLayer 的 RenderLayer 会被绘制到自己的缓存里面,而没有 GraphicsLayer 的 RenderLayer 它们会向上追溯有 GraphicsLayer 的父/祖先 RenderLayer,直到 Root RenderLayer 为止,然后绘制在有 GraphicsLayer 的父/祖先 RenderLayer 的缓存上,而 Root RenderLayer 总是会创建一个 GraphicsLayer 并拥有自己独立的缓存。最终,GraphicsLayer 又构成了一棵与 RenderLayer 并行的树,而 RenderLayer 与 GraphicsLayer 的关系有些类似于 RenderObject 与 RenderLayer 之间的关系
- 根节点、css定位、3D 变换、filter、页面滚动,或者使用 z-indexing 做 z 轴排序等,还有比如是含有层叠上下文如何控制显示和隐藏等情况、canvas、video
- 浏览器渲染引擎遍历 Layer 树,访问每一个 RenderLayer,再遍历从属于这个 RenderLayer 的 RenderObject,将每一个 RenderObject 绘制出来。Layer 树决定了网页绘制的层次顺序,而从属于 RenderLayer 的 RenderObject 决定了这个 Layer 的内容,所有的 RenderLayer 和 RenderObject 一起就决定了网页在屏幕上最终呈现出来的内容。
- 合成加速的渲染架构下,Layer 的内容变化,只需要更新所属的 GraphicsLayer 的缓存即可,而缓存的更新,也只需要绘制直接或者间接属于这个 GraphicsLayer 的 RenderLayer 而不是所有的 RenderLayer
- 特别是一些特定的 CSS 样式属性的变化,实际上并不引起内容的变化,只需要改变一些 GraphicsLayer 的合成参数,然后重新合成即可,而合成相对绘制而言是很快的
- 合成加速跟非合成加速的主要区别是网页全部的 Layer 只使用一个缓存,还是一些特定的 Layer 拥有自己独立的缓存成为合成图层(Compositing Layer)
- chrome的layers能看到哪些layer是一个合成图层(Compositing Layer)




HTML根元素本身就具有层叠上下文。
普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
元素的 opacity 值不是 1
元素的 transform 值不是 none
元素的 filter 值不是 none
元素的 isolation 值是isolate
will-change指定的属性值为上面任意一个。(will-change的作用后面会详细介绍)
需要剪裁的地方(比如超出的文字部分)、滚动条
z-index比较低的节点会提升为一个单独的图层,那么层叠等级比它高的节点都会成为一个独立的图层
绘制图层
浏览器在渲染图形的时候,有一个绘图上下文,绘图上下文又分成两种类型:
- 第一种是用来绘制2D图形的上下文,称之为2D绘图上下文(GraphicsContext)
- 第二种是绘制3D图形的上下文,称之为3D绘图上下文(GraphicsContext3D)
网页也有三种渲染方式
- 软件渲染(CPU内存)
- 使用软件绘图的合成化渲染(GPU内存)CSS3D、WebGL
- 硬件加速的合成化渲染(GPU内存)
图层绘制流程:
- 会把一个复杂的图层拆分为很小的绘制指令,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。
- 绘制图层的操作在渲染进程中有着专门的线程,这个线程叫做合成线程
- 所谓硬件加速,就是使用 GPU 来进行合成,绘制仍然使用 CPU 来完成。
- 硬件加速和非硬件加速的区别是,网页缓存输出到窗口缓存的缓存合成过程是由 GPU 还是由 CPU 来完成的,如果没有合成加速,网页缓存输出到窗口缓存的缓存合成过程其实就是简单的拷贝一个网页缓存到窗口缓存,如果使用合成加速,这个过程就涉及到多个缓存的拷贝,包括可能的 2D/3D 几何变换(位移,旋转,缩放等)和 Alpha 混合。

图层分块
-
合成线程会将图层划分为图块(tile)
- 假设需要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销
-
这些块的大小一般不会特别大,通常是 256 * 256 或者 512 * 512 这个规格。这样可以大大加速页面的首屏展示
- 在首次合成图块时只采用一个低分辨率的图片,这样首屏展示的时候只是展示出低分辨率的图片,这个时候继续进行合成操作,当正常的图块内容绘制完毕后,会将当前低分辨率的图块内容替换。这也是 Chrome 底层优化首屏加载速度的一个手段。
- 所谓 GPU 合成,通常是使用 Open GL/ES 贴图来实现的,而这时的缓存其实就是纹理(GL Texture),而很多 GPU 对纹理的大小有限制,比如长/宽必须是2的幂次方,最大不能超过2048或者4096等,所以无法支持任意大小的缓存
- 使用小块缓存,方便浏览器使用一个统一的缓存池来管理分配的缓存,这个缓存池一般会分配成百上千个缓存块供所有的 WebView 共用。所有打开的网页,需要缓存时都可以以缓存块为单位向缓存池申请,而当网页关闭或者不可见时,这些不需要的缓存块就可以被回收供其它网页使用;
光栅化
- 合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
- 合成线程会选择视口附近的图块(tile),把它交给栅格化线程池生成位图
- 渲染进程中专门维护了一个栅格化线程池,专门负责把图块转换为位图数据
- 生成位图的过程实际上都会使用 GPU 进行加速,生成的位图最后发送给合成线程
- 用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示
- 栅格化操作完成后,合成线程会生成一个绘制命令,即"DrawQuad",并发送给浏览器进程。
- 浏览器进程中的viz组件接收到这个命令,根据这个命令,把页面内容绘制到内存,也就是生成了页面,然后把这部分内存发送给显卡。
- 无论是 PC 显示器还是手机屏幕,都有一个固定的刷新频率,一般是 60 HZ,即 60 帧,也就是一秒更新 60 张图片,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将前缓冲区和后缓冲区对换位置,如此循环更新。
- 显卡的职责就是合成新的图像,并将图像保存到后缓冲区中,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显示器能读取到最新显卡合成的图像。通常情况下,显卡的更新频率和显示器的刷新频率是一致的
- 某个动画大量占用内存时,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象

显示器读取图片和浏览器生成图片不同步问题
- 如果渲染进程生成的帧速比屏幕的刷新率慢,那么屏幕会在两帧中显示同一个画面,当这种断断续续的情况持续发生时,用户将会很明显地察觉到动画卡住了。
- 如果渲染进程生成的帧速率实际上比屏幕刷新率快,那么也会出现一些视觉上的问题,比如当帧速率在 100fps 而刷新率只有 60Hz 的时候,GPU 所渲染的图像并非全都被显示出来,这就会造成丢帧现象。
- 就算屏幕的刷新频率和 GPU 更新图片的频率一样,由于它们是两个不同的系统,所以屏幕生成帧的周期和 VSync 的周期也是很难同步起来的。
为了解决这些问题,就需要将显示器的时钟同步周期和浏览器生成页面的周期绑定起来
- 当显示器将一帧画面绘制完成后,并在准备读取下一帧之前,显示器会发出一个垂直同步信号(vertical synchronization)给 GPU,简称 VSync。这时候浏览器就会充分利用好 VSync 信号
- 当 GPU 接收到 VSync 信号后,会将 VSync 信号同步给浏览器进程,浏览器进程再将其同步到对应的渲染进程,渲染进程接收到 VSync 信号之后,就可以准备绘制新的一帧了
- 在合成完成之后,合成线程会提交给渲染主线程提交完成合成的消息,如果当前合成操作执行的非常快,比如从用户发出消息到完成合成操作只花了 8 毫秒,因为 VSync 同步周期是 16.66(1/60)毫秒,那么这个 VSync 时钟周期内就不需要再次生成新的页面了
- 从合成结束到下个 VSync 周期内,就进入了一个空闲时间阶段,那么就可以在这段空闲时间内执行一些不那么紧急的任务,比如 V8 的垃圾回收,或者通过 window.requestIdleCallback() 设置的回调任务等,都会在这段空闲时间内执行
关于 css 和 js 脚本加载是否阻塞 dom 解析
css 脚本:
- 如果 css 是写在 style 标签中,那么在 chrome 中 style 标签会被 Html Parse 解析器来解析,也就是说会阻塞
- 如果是 link 标签引入 css 文件,由于 CSSOM 的生成并不会影响 DOMTree 的改变(js脚本会,因为可能会修改 dom 节点),所以 HTML Parse 遇到 link 标签的 stylesheet 时并不会等待 stylesheet 下载并解析完毕后才会解析后续 Dom,所以 link 引入 css 文件并不会阻塞 dom 解析
- 当网络进程加载完 link 引入的样式脚本后,主线程中仍然需要存在一个 parse styleSheet 的操作,这一步就是解析 link 脚本中的样式内容从而生成(添加)CSSOM 上的节点,但是 parse styleSheet 的操作是在主线程中进行操作的。这也就意味着它会和 parse Html 抢占主线程资源(同一时间只能进行一个操作),所以加载完成后会阻塞
- 特殊情况下,比如 link 的样式脚本后存在非 defer 以及 async 标记的异步脚本 JS 文件,那么此时 Css 代码的加载是会阻塞后续 JS 脚本的执行从而 JS 脚本会阻塞后续 Dom 的解析,从而变相相当于阻塞了 Js 代码之后的 Dom 解析
- <link rel=“preload” as=“style” href=“style.css” /> 的情况下不会

js 脚本:
- 内联脚本:放在哪里都是会阻塞页面的渲染,不过是放在底部在脚本中可以拿到内存中已经构造好的 Dom 节点进行 Dom 操作而已
- 可以看到,DCL(DomContentLoaded)之后才会出现 FP(First Paint 首次绘制时间点)
- 外部脚本链接:加载和执行只会影响后续 Dom 的解析和渲染,对于脚本之前的的 Dom 并不会阻塞它的解析以及渲染,这也就是为什么我们常说将 js 放在底部。
- async & defer的情况:
- defer:文档完成解析后,触发 DOMContentLoaded 事件前执行,文档解析完毕会立即触发一次渲染之后才会去依次执行标记为 defer 的脚本。也就是标记为 defer 的脚本并不会阻塞页面的首次渲染
- async:因为并没有具体的触发时机, async 究竟会阻塞哪些节点渲染在不同网络条件下是不尽相同的
关于 css 脚本是否阻塞页面渲染
- 内联 css 因为会被 Html Parse 解析器来解析,所以说会阻塞
- 外联 css
- 将 link 放到 head 中会阻塞
- 因为此时本身就没有任何内容,所以阻塞解析 css 内容是合理的
- 将 link 放到 body 最后,不会阻塞,会先生成没有样式的页面,等解析完成后,回流重绘再生成最新的页面
- 当主线程 Parse Html 的过程中遇到 link 标签会立即进行一次绘制,此时已经解析好的 DomTree 会被认为 Cssom 依赖
- <link rel=“preload” as=“style” href=“style.css” /> 的情况下不会
- 将 link 放到 head 中会阻塞
758

被折叠的 条评论
为什么被折叠?



