前端渲染原理

前端渲染原理
TheStoneFish现代游览器
一、游览器架构
1、早期游览器
早期游览器使用的单进程单线程
缺点
- 不安全
所有模块共享进程权限(如文件读写),恶意插件或脚本可直接利用高权限攻击系统。 - 不流畅
核心任务(如渲染、JS 执行)运行在同一线程,长时间任务(复杂 JS 计算)会阻塞线程,导致页面卡顿或无响应。 - 不稳定
单进程内模块耦合紧密,任一组件(如插件)崩溃会直接导致整个浏览器崩溃,多标签页同时受影响。
2、现代游览器
现代游览器使用多进程, 谷歌游览器会为每一个标签页启动一个进程运行(渲染进程)
现代游览器的进程
进程 | 负责 |
---|---|
browser | 浏览器基本功能,包括导航栏、导航按钮、书签、网络请求、文件读写等。 |
renderer * n | tab 内和网页展示相关的所有工作,主要是将 HTML,CSS,以及 JavaScript 转变为我们可以进程交互的网页内容。 Chrome 为每一个 tab 甚至是页面里面的每一个 iframe 都分配一个单独的进程 |
Extension * n | 每个扩展程序可运行在独立的进程中,确保安全隔离。 |
GPU | 处理来自不同 tab 的渲染请求并把它在同一个界面上画出来 |
Network | Chrome将网络请求独立为一个进程,管理缓存、Cookie 和资源加载 |
优点
更高的稳定性
- 进程隔离:Chrome 将浏览器的各个组件(如渲染、插件、GPU等)分配到不同的进程中,这样一个进程崩溃时不会影响其他进程。例如,如果一个标签页崩溃,其他标签页和浏览器本身仍然能够正常工作。
- 标签页隔离:如果一个网页或标签页的渲染进程出现问题,它的崩溃通常不会影响到其他标签页。这减少了因单个网页错误导致整个浏览器崩溃的风险。
安全性增强
- 跨站攻击防护:多进程架构有助于保护用户免受跨站脚本攻击(XSS)和跨站请求伪造(CSRF)等安全问题。
- 沙箱技术:每个渲染进程(也就是每个标签页的内容)、插件都运行在沙箱环境中,限制了恶意代码的执行范围。即使某个网页包含恶意代码,攻击者也很难通过该网页访问操作系统或其他网页的数据。
缺点
- 内存和资源消耗
- 高内存消耗:多进程架构的一个显著缺点是,它会消耗更多的内存资源。每个进程都需要分配一定的内存空间,而 Chrome 在每个标签页、插件、扩展甚至网络请求上通常都会启动独立的进程。尽管浏览器会尽量合并同一网站的多个标签页,但整体上仍然会导致内存使用量的增加,尤其是在打开大量标签页时。
- 进程管理开销:操作系统需要管理多个进程,这会带来一定的系统开销。进程间的上下文切换和通信需要消耗额外的资源,尤其是在资源紧张的环境下,可能会导致性能下降。
- 进程间通信的复杂性
- IPC(进程间通信)开销:由于每个进程运行在独立的内存空间中,它们之间需要通过 IPC(进程间通信) 来交换数据。这种通信往往比较复杂,可能导致效率低下,特别是在需要频繁交换数据时。例如,浏览器的渲染进程和浏览器进程需要通过 IPC 共享网页数据和状态,增加了复杂性和性能负担。
二、渲染进程(多线程)
渲染进程的工作
渲染进程的工作包括:
- 解析HTMl
- 解析CSS
- 计算样式
- 布局
- 处理图层
- 绘制
- 执行JS
- 执行事件回调
- 执行计时器的回调
- ……
三、事件循环
异步任务在主线程执行吗?
任务由其他线程来完成, 其他线程再向消息队列提交任务
比如计时器任务由外部计时器线程进行计时, 计时完成后向消息队列末尾添加任务
事件循环是异步的解决方案
事件循环(Event Loop)是用于处理异步操作的机制,它是一种循环过程, 异步任务处理完后回调会加入消息队列挂起, 等主线程空闲时会执行队列中的任务。
异步任务包括(setTimeout、SetInterval、XHR、Fetch、addEventListener)
1 | while (queue.waitForMessage()) { |
消息队列类型
每个任务都有一个任务类型,
同⼀个类型的任务必须在⼀个队列,
一个队列可以有不同类型的任务
- 延时队列:⽤于存放计时器到达后的回调任务,优先级「中」
- 交互队列:⽤于存放⽤户操作后产⽣的事件处理任务,优先级「⾼」
- 微队列:⽤户存放需要最快执⾏的任务,优先级「最⾼」
四、渲染流程
HTML字符串 -> 解析HTML -> 样式计算 -> 布局 -> 分层 -> 绘制 -> 分块 -> 光栅化 -> 画 -> 像素信息
当游览器的网络线程收到HTML文档后, 会产生一个渲染任务, 并加入消息队列, 渲染主线程取出消息队列的渲染任务, 开始进行渲染。
1、解析html
HTML的解析:边下载边解析
- 流式解析:浏览器采用**增量解析(Incremental Parsing)**策略,在接收到HTML数据时立即开始解析,无需等待整个文件下载完成。这种机制允许浏览器逐步构建DOM树,优化页面加载速度。
- 阻塞情况:当遇到外部JavaScript脚本(未标记
async
或defer
的<script>
标签)时,HTML解析会暂停,直到脚本下载并执行完成。这是因为脚本可能修改DOM结构,需确保解析正确性。- 渲染机会:浏览器在解析过程中可能多次触发渐进式渲染,将已解析的部分内容先行显示给用户,提升体验。
CSS的解析:下载完成后解析
- 全量解析:浏览器需等待完整的CSS文件下载完成后,才开始解析并构建CSSOM(CSS Object Model)。这是因为CSS的层叠规则(如后续样式覆盖前面样式)需要全局信息才能正确计算。
- 阻塞渲染:CSS解析会阻塞页面渲染(但通常不阻塞HTML解析),以避免“无样式内容闪烁”(FOUC)。浏览器在CSSOM构建完成后,才会将DOM与CSSOM合并为渲染树,触发布局和绘制。
- 并行运行:尽管CSS解析需全量数据,但其文件下载和解析是并行进行的,不会阻塞HTML解析(除非后续脚本依赖CSSOM)。
渲染主线程会解析接收的html字符串, 遇到同步的css和js会直接执行。
如果解析到错误的格式, 不会进行报错, 解析会自动进行处理, 处理规则
预解析线程
- 并行处理:当主解析器(Main Parser)逐行构建DOM树时,
预解析线程
会快速扫描后续的HTML内容,识别需要加载的外部资源(如脚本、样式表、图片等)。 - 提前下载:
预解析线程
发现资源链接后,立即触发资源的下载,而无需等待主解析器处理到相应位置。这减少了资源加载的等待时间。 - CSS解析:css的解析和下载都在预解析器内进行, 不会阻塞主线程解析html。
解析的过程遇到外部link标签
link标签的解析在预解析线程进行, 不会阻塞主线程解析html
把link放在head
css尽量放在head, 提前加载样式, 不然可能会导致回流和重绘, 页面布局加载出来, 但是页面样式还没渲染(FOUC), 样式闪烁
css放在body会导致两次渲染
- 在第一次渲染中,仅加载和渲染最低限度的 HTML。
- 在第二次渲染中,将应用样式并再次渲染页面。这可能会使 HTML 元素的大小、形状、颜色发生变化,并可能使网页闪烁。
解析到</body>
的时候会进行安排重新计算样式的时间
这里虽然link放在head不会阻塞HTMl的解析, 但是会阻塞渲染树的构建,因为渲染树需要css的构建完成。
1 |
|
把link放在body
js尽量放在body结束之前或者使用defer属性和anysc, 因为js阻塞了DOM构建, 会导致js内部获取DOM元素获取失败, 页面加载更慢, 因为需要等待js下载并且执行, 这样没有的异步, ansyc属性可以让js下载好后执行, defer可以让js等待DOM构建完成后执行。
这里解析到link会阻塞HTMl的解析(触发加上media)
然后会触发一次渲染, 仅加载和渲染最低限度的 HTML(渐进式渲染)。
1 |
|
解析的过程遇到外部script标签
只有script
在解析中遇到<script>
标签会暂停HTML解析, 因为js有可能改变页面结构document.write()
, 必须等js执行后才会继续渲染 所以建议把js放在body结束标签之前, 这样对阻塞的影响较小, 设置defer属性后可以不进行阻塞, ansyc属性可以让js下载好后执行
遇到script标签暂停html的解析, 下载并且执行js
1 |
|
1 | function sleep(ms) { |
同时有script和link
遇到script, 如果前面有link标签没有被解析会等待下载和解析完成后执行js, 因为js可能操作css属性, 所以必须完成css解析后执行js
1 |
|
2、DOM构建
构建DOM,并且提供DOM操作DOM的接口。
1 | <html> |
3、CSSOM构建
这一步构建CSSOM并且提供给js处理css的接口。
通过document.styleSheets
可以查看最后的结构, 每增加一个样式表(内联, 内部, 外部, 游览器默认样式表), styleSheets子节点就会增加一个
4、样式计算
样式来源
- 用户代理样式表:浏览器默认的样式表
- 用户样式表:用户自定义的样式表
- 作者样式表:开发者编写的样式表
优先级顺序为:
- 作者样式表 > 用户样式表 > 用户代理样式表
标准化阶段
在此阶段,非标准单位会被转换为标准单位。例如:
width: 80%
转为width: 1000px
color: red
转为color: rgb(255, 0, 0)
二、属性计算
根据以下优先级规则来计算CSS属性值:
- !important: 最高优先级
- Inline Style: 内联样式,优先级高于作者样式表
- 明确性: CSS选择器的优先级
- 继承性: 某些属性可以继承(如
color
,font-*
,line-*
等) - 浏览器默认样式: 最低优先级
解析样式表
查看作者样式表:
- 如果有样式,根据优先级(!important > inline-style > 明确性)来计算。
- 如果多个样式选择器有相同优先级,后面的样式会覆盖前面的样式。
查看继承的属性:
- 如果作者样式表没有定义某些属性,则检查是否有继承的属性(如
color
,font-*
,line-*
等可继承属性)。
查看用户样式表:
- 检查用户样式表中是否有覆盖或定义的属性。
查看用户代理样式表:
- 用户代理样式表包含浏览器为标准HTML元素定义的默认样式(如
h1
,a
等元素的默认样式)。
示例(来自 Chromium源代码):
1 | h1 { |
最后生成计算后的css样式(Computed Style), 每个元素的所有css样式都有会值,
通过getComputedStyle可以获取计算样式。
5、生成Layout树
等待CSSOM和DOM生成完成后会结合它们变为Layout树, Layout树包含了元素的位置
和大小
, 位置是相对于包含块的位置, Layout里面的对象是c++对象, 无法被js获取的, 只能获取document.body.clientWidth
, document.body.clientHeight
等部分信息。
元素 | DOM树 | Layout树 |
---|---|---|
display: none | ✅ | ❌ |
::after | ❌ | ✅ |
匿名盒 | ❌ | ✅ |
由于:
- 内容必须在行盒内
- 行盒和块盒不能相邻
所以下面的代码会在dom和layout树中呈现不同的结构
1 | <div> |
6、分层 Layer
游览器会使用一套复杂的策略堆整个布局树中进行分层。
分层是为避免页面更新后, 整个页面进行重新绘制, 仅对该层处理, 比如滚动条单独为一层, 当对页面上的元素进行某些操作(比如3D变换)时也会进行分层。
叠上下文有关的属性会影响分层的结果: z-index, opacity, transform, 也可以通过will-change更大程度来影响分层的结果。
1 |
|
元素 | 原因 | 原因 |
---|---|---|
document | is the document.rootScroller. is a scrollable overflow element using accelerated scrolling. |
作为 document.rootScroller 成为浏览器默认的滚动容器 |
test1 | has an active accelerated transform animation or transtion. | 正在执行 transform 相关的 CSS 动画/过渡 |
test2 | has a will-change: transform compositing hint. | will-change: transform 显式声明变化预期 |
7、绘制 Paint(下面的工作不是主线程来完成了)
将每一层的结果转化为一堆绘制指令集, 用于描述这一层内容该如何绘制。
8、分块 Tiling
完成绘制后, 主线程将每个图层的绘制信息提交给合成线程
, 剩余工作交给合成线程来完成。
分块会将每一层分为多个小的区域(通常是 256x256 像素的 Tile), 分块的工作是交给多个线程同时进行的, 合成线程会从线程池启动多个分块线程来完成。
9、光栅化 Raster
合成线程会将块信息交给GPU进程
, 以极高的速度完成光栅化。
GPU进程会开启多个线程来完成光栅化, 并且优先处理靠近视口的块。
光栅化的结果是一块一块的位图。
10、画 Draw
合成线程拿到每个层、每个块的位图后, 生成一个个指引(quad)信息。
指引会标识出每个位图应该画到屏幕的哪个位置, 以及会考虑到旋转和缩放等变形。
合成线程会把quad提交给GPU进程, 由GPU进程产生系统调用, 提交给GPU硬件, 完成最终的屏幕成像。
因为合成线程工作在渲染进程中, 并且渲染进程工作在沙盒环境
, 所以需要先提交给GPU进程, 然后GPU进程再交给显卡渲染。
五、回流 reflow
reflow会影响布局, 它的本质就是重新计算Layout树。
当进行了影响布局树的操作后, 需要重新计算布局树, 为了避免连续的多次操作导致布局树反复计算, 游览器会合并这些操作, 当js代码全部执行完成后再进行统一计算。
所以JS获取布局属性的时候, 可能无法获取到最新的布局信息, 所以游览器决定获取布局属性的时候, 立即reflow。
六、重绘 repaint
repaint不会影响布局, 它的本质就是根据分层信息重新计算了绘制指令。
当改动了可见样式后, 就需要重新计算, 会引发repaint。
reflow一定会导致repaint。
七、transform
transform改变后只需要在合成线程
画的过程中重新计算一下位置, 不影响主线程的使用, 所以transform不会阻塞主线程, 滚动条也是只需要重新计算一下绘画的位置, 所以主线程被阻塞也不会影响transform和滚动条的运行。
1 |
|
1 |
|