浏览器详解(四) 渲染

大家好,我是半虹,这篇文章来讲浏览器渲染


1、基本介绍

浏览器是多进程多线程的架构,包括有浏览器进程、渲染器进程、GPU 进程、插件进程等

在上篇文章中我们介绍过浏览器进程,作为浏览器主进程,负责浏览器基本界面的交互和底层功能的执行

在这篇文章中我们来介绍渲染器进程,这是浏览器的内核,也称为渲染进程

渲染进程负责标签页内所发生的事情,主要包括页面渲染、脚本执行、事件处理等

其中包含多个重要的线程以协同工作:

  1. GUI 渲染线程:负责页面的渲染,解析 HTML、CSS,构建 DOM、CSSOM,布局和绘制等

            如果页面要回流或重绘,也由该线程去执行

  2. JS 引擎线程:负责解析和执行 JavaScript 脚本,注意 JS 引擎线程与 GUI 渲染线程互斥

  3. 事件触发线程: 控制事件循环,并管理事件队列

            若当 JS 引擎执行触发异步事件,会将对应的任务交给相应的线程执行处理

            等到任务完成之后,把任务对应的回调添加到事件队列,等待 JS 引擎处理

  4. 定时触发线程: 负责处理特定任务,具体来说就是用于 setTimeout 和 setInterval 计时

            等到计时结束,通知事件触发线程,由事件触发线程将对应的回调加入队列

  5. 异步请求线程: 负责处理特定任务,具体来说就是处理 AJAX 请求

            等到请求完成,通知事件触发线程,由事件触发线程将对应的回调加入队列

下面我们通过例子,异步请求和定时操作,来说明线程之间如何协同工作

上述多个线程大体上可以分成两组:

  1. 渲染引擎:其中核心为 GUI 渲染线程
  2. 脚本引擎:其中核心为 JS 引擎线程,辅助为事件触发线程、定时触发线程、异步请求线程

目前市面主流浏览器使用的渲染引擎和脚本引擎总结如下:

浏览器 渲染引擎 脚本引擎
Safari Webkit / WebCore Webkit / JavaScriptCore
Chrome Blink V8
Firefox Gecko SpiderMonkey → TraceMonkey → JaegerMonkey
IE Trident Chakra
Edge EdgeHTML Chakra

在这篇文章中我们主要介绍渲染过程,至于 JS 的解析过程,以后有空的话会专门再讲

好了,下面来正式开始介绍浏览器中的渲染过程

2、渲染流程

渲染引擎渲染页面的过程,也称为关键渲染路径,具体步骤如下:

  1. 解析 HTML 构建 DOM

    具体步骤如下:

    转换:从网络或磁盘读取 HTML 文件原始字节,根据文件编码将字节转换为字符串

    分词:根据 HTML 规范,将上述字符串切分为不同的标记,如 <html> 、 <body>

    语法 分析:将上述标记转换为对象,对象中包含语法信息,如属性、属性值等信息

    DOM 构建:将上述对象链接在树状的结构中以标识父子和兄弟关系,这个树状结构就是 DOM

    需要注意的是:

    在读取 HTML 时,DOM 的构建已经开始,可以将上面的步骤理解成是流水线并行

    上一个步骤每完成一点,就会将结果传递给下一个步骤,不会等一个步骤完全执行完才进行下一个步骤

    关于脚本资源:

    当 HTML 解析器遇到 <script> 标签时,会暂停 HTML 的解析,并开始加载、解析和执行 JavaScript

    这是因为 JavaScript 可能会修改 DOM 的结构,若明确 JavaScript 不会修改 DOM, 那么:

    你可以为 <script> 标签加 async 属性,这样是异步加载 JavaScript,但是加载完马上就会开始执行

    也可以为 <script> 标签加 defer 属性,这也是异步加载 JavaScript,且等 DOM 构建完才开始执行

    其实如果 JavaScript 确实要修改 DOM 某节点,那也该将其放到 DOM 的构建之后

    否则有些获取元素的 操作可能就不能生效

    关于外部资源:

    一个网页通常包含多个外部资源,例如图片、CSS、JavaScript、字体等等

    在解析 DOM 过程中,会按文档的顺序逐一发起请求 ,但是为了提高效率会同时运行预加载扫描器

    预加载扫描器查看已切分的标记,从中寻找资源地址并告知网络线程请求

    如此当 HTML解析被 JavaScript 阻塞时,预加载扫描器也能继续请求资源

  2. 解析 CSS 构建 CSSOM

    具体步骤如下:

    搜集:从内联样式、内部样式表、外部样式表、浏览器代理样式中,搜集所有样式规则

    构建:按上述规则构建 CSSOM 树状结构,其中每个节点表示一个样式规则,包含选择器以及样式声明

    匹配:遍历 DOM 节点匹配选择器,并为匹配的节点执行样式设置

    计算:计算 DOM 节点的最终样式,这些样式可通过以下接口查看:window.getComputedStyle()

    需要注意的是:

    上述匹配步骤,CSS 选择器会从右往左匹配 DOM 节点,举个例子,假设 CSS 选择器为 .nav span

    那么匹配之时,首先找到所有 span 节点,然后对每个 span 节点,向上找 .nav 节点

    另外注意的是:

    构建 DOM 和构建 CSSOM 是同时进行的,但是这并不意味着二者不会相互影响

    因为 CSSOM 构建会阻塞脚本执行,而脚本执行会阻塞 DOM 构建,有一个间接的影响

  3. 布局 (Layout)

    有了 DOM 和 CSSOM 后,需要根据二者构建出渲染树 (RenderObject Tree)

    渲染树只会包含可见元素,像 head 节点和 display 为 none 的节点,则不会出现在渲染树中

    至此,其实已经知道每个可见节点的样式,接下来需要根据渲染树进行布局计算

    即根据盒子模型和视觉格式化模型来计算每个节点在设备视口内的位置以及大小

    当页面布局和几何信息发生变化时,浏览器需要重新布局,这个过程被称为回流 (reflow)

    具体又可分为全局布局和局部布局,常见的引起回流阶段的操作如下:

    • 页面第一次进行渲染
    • 窗口的尺寸发生改变
    • 增加或删除可见元素
    • 元素的位置发生改变
    • 元素的大小发生改变
    • ...
  4. 绘制 (Paint)

    绘制实际上就是遍历渲染树,在屏幕中绘制出对应的节点内容,简单来说就是个像素填充的过程

    绘制过程在多个层上完成的,这些层被称为渲染层 (Render Layer)

    渲染树中每个节点都是一个渲染对象,处于相同坐标空间 (z 轴) 的渲染对象会合并到同一个渲染层

    对于满足层叠上下文条件的渲染对象,浏览器会为其自动创建新的渲染层,常见情况如下:

    • 根元素
    • 有 mask 属性
    • 有 filter 属性
    • 有 reflection 属性
    • 有 position 属性且值为 absolute、relative、fixed、sticky
    • 有 backface-visibility 属性且值为 hidden
    • 有 transform 属性且值不为 none
    • 有 overflow 属性且值不为 visible
    • 有 opacity 属性且值小于 1
    • 对 opacity、transform、filter 等属性应用动画

    满足上述条件的渲染对象拥有独立的渲染层,但这并不意味着这些渲染对象独享该渲染层

    这是因为不满足上述条件的渲染对象将会与第一个拥有渲染层的父元素共用同一个渲染层

    当元素样式有变化但不影响布局时,浏览器需要重新绘制,这个过程被称为重绘 (repaint)

    具体又可分为全局绘制和局部绘制,常见的引起重绘阶段的操作如下:

    • 修改元素 color
    • 修改元素 background-color
    • ...

    值得注意的是:回流一定引起重绘,重绘不一定引起回流

  5. 合成 (Composite)

    合成阶段中,多个渲染层按照恰当的重叠顺序进行合并,而后生成位图,最终通过显卡展示在屏幕上

    在这过程中,某些渲染层会被浏览器自动提升为合成层 (Compositing Layer), 常见情况如下:

    • 有 will-change 属性
    • 有 position 属性且值为 fixed
    • 有 transform 属性且值为 translate3d、translateZ 等 3D 变换
    • 是 canvas、video、iframe 等元素
    • 对 opacity、transform、filter 等属性应用动画

    每个合成层拥有单独的 Graphics Layer 用于生成位图,并上传到 GPU 对其进行合成,绘制到屏幕上

    其它不是合成层的渲染层与第一个拥有 Graphics Layer 的父层级共用

    合成层有什么用?答案是硬件加速,即把某些需要频繁回流和重绘的元素单独提升成为合成层

    这样不仅可以提高绘制的效率,也能减少这些元素修改时对其它元素影响

    例如修改合成层的 transform 时,不会带来回流或重绘,只需要将多个图层再次合并,而后生成位图

    这样我们可以通过 transform:translate 修改元素位置,通过 transform:scale 修改元素大小

渲染进程的工作对浏览器性能有重要影响,常见渲染优化包括:

  1. 关键样式资源放在头部加载,善用 preload、prefetch
  2. 脚本资源通常放在脚部加载,善用 async、defer
  3. 尽量减少回流以及重绘操作,例如 批量修改 DOM 、离线修改 DOM 等等
  4. 对于画布,常用的技术包括有分层 canvas、离屏 canvas 等等
  5. 对于动画,使用 requireAnimationFrame 代替 setTimeout
  6. 硬件加速
  7. ......

类似方法有很多,这些都需要在日常开发中不断地积累,并有意识地应用到未来开发中


好啦,本文到此结束,感谢您的阅读!

如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议

如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)

相关推荐
CodeSheep6 分钟前
朋友面试谈薪报价2w,HR非得压到1.9,还反问他:你就差这1000块钱?后来背调时问了他前同事十几个问题,对方最后直接挂了
前端·后端·程序员
IMPYLH13 分钟前
HTML 的 <track> 元素
前端·html
涛涛ing14 分钟前
35个组件,把GPU特效直接跑在真实DOM上:Canvas UI正在重新定义Web视觉
前端
乘风gg15 分钟前
Spec Kit vs OpenSpec vs Superpowers:8 个 Skill 的落地实录与工程方法论
前端·ai编程·claude
一颗小芹菜的日常44 分钟前
vue-crack插件是干什么的
前端·javascript·vue.js
小比特-combat1 小时前
LVGL_1(使用示例讲解父子关系)
开发语言·前端·javascript
IT_陈寒1 小时前
Java线程池的坑把我埋了,踩出来的血泪教训
前端·人工智能·后端
月夏1 小时前
Node.js 读 Excel 我踩了 3 个坑,最后一个查遍 Stack Overflow 没答案
前端·javascript
亿元程序员1 小时前
PSD都不用了!Codex直接生成Cocos预制体
前端
柳杉1 小时前
Codex + 可视化大屏工作流实践:15 个行业场景的设计产出合集
前端·openai·数据可视化