【极客时间】浏览器工作原理与实践-2 宏观视角下的浏览器 (6讲) - 2.6 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?
https://time.geekbang.org/column/article/118826
2.6 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?
2.5介绍了渲染流水线中的 DOM 生成、样式计算和布局三个阶段,2.6讲解渲染流水线后面的阶段。
简单回顾下2.5前三个阶段的主要内容:
- 在 HTML 页面内容被提交给渲染引擎之后,渲染引擎首先将 HTML 解析为浏览器可以理解的 DOM;
- 然后根据 CSS 样式表,计算出 DOM 树所有节点的样式;
- 接着又计算每个元素的几何坐标位置,并将这些信息保存在布局树中。
分层
有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?
答案依然是否定的。
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。
要想直观地理解什么是图层,你可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,如下图所示:
从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面,你可以参考下图:
现在你知道了浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。下面我们再来看看这些图层和布局树节点之间的关系,如文中图所示:
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
如上图中的 span 标签没有专属图层,那么它们就从属于它们的父节点图层。
但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。
那么需要满足什么条件,渲染引擎才会为特定的节点创建新的图层呢?
通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
第一点,拥有层叠上下文属性的元素会被提升为单独的一层
页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。你可以结合下图来直观感受下:
从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。若你想要了解更多层叠上下文的知识,你可以参考这篇文章。
第二点,需要剪裁(clip)的地方也会被创建为图层
不过首先你需要了解什么是剪裁,结合下面的 HTML 代码:
<style>div {width: 200;height: 200;overflow:auto;background: gray;}
</style>
<body><div ><p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p><p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p><p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> </div>
</body>
在这里我们把 div 的大小限定为 200 * 200 像素,而 div 里面的文字内容比较多,文字所显示的区域肯定会超出 200 * 200 的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域,下图是运行时的执行结果:
出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。你可以参考下图:
所以说,元素有了层叠上下文的属性或者需要被剪裁,满足其中任意一点,就会被提升成为单独一层。
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?
试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?
通常,你会把你的绘制操作分解为三步:
- 绘制蓝色背景;
- 在中间绘制一个红色的圆;
- 再在圆上绘制绿色三角形。
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:
从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
你也可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表,如下图所示:
在该图中,区域 1 就是 document 的绘制列表,拖动区域 2 中的进度条可以重现列表的绘制过程。
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:
如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?
那我们得先来看看什么是视口,你可以参看下图:
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示:
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:
从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫== viz 的组件,用来接收合成线程发过来的 DrawQuad 命令==,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。
渲染流水线大总结
好了,我们现在已经分析完了整个渲染流程,从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程:
结合上图,一个完整的渲染流程大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
相关概念
有了上面介绍渲染流水线的基础,我们再来看看三个和渲染流水线相关的概念——“重排”“重绘”和“合成”。理解了这三个概念对于你后续 Web 的性能优化会有很大帮助。
1. 更新了元素的几何属性(重排)
从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
2. 更新元素的绘制属性(重绘)
接下来,我们再来看看重绘,比如通过 JavaScript 更改某些元素的背景颜色,渲染流水线会怎样调整呢?你可以参考下图:
从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
3. 直接合成阶段
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图:
在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
至于如何用这些概念去优化页面,我们会在后面相关章节做详细讲解的,这里你只需要先结合“渲染流水线”弄明白这三个概念及原理就行。
总结
通过本文的分析,你应该可以看到,Chrome 的渲染流水线还是相当复杂晦涩,且难以理解,不过 Chrome 团队在不断添加新功能的同时,也在不断地重构一些子阶段,目的就是让整体渲染架构变得更加简单和高效,正所谓大道至简。
通过这么多年的生活和工作经验来看,无论是做架构设计、产品设计,还是具体到代码的实现,甚至处理生活中的一些事情,能够把复杂问题简单化的人都是具有大智慧的。所以,在工作或生活中,你若想要简化遇到的问题,就要刻意地练习,练就抓住问题本质的能力,把那些复杂的问题简单化,从而最终真正解决问题。
思考时间
在优化 Web 性能的方法中,减少重绘、重排是一种很好的优化方式,那么结合文中的分析,你能总结出来为什么减少重绘、重排能优化 Web 性能吗?那又有那些具体的实践方法能减少重绘、重排呢?
相关文章:

【极客时间】浏览器工作原理与实践-2 宏观视角下的浏览器 (6讲) - 2.6 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?
https://time.geekbang.org/column/article/118826 2.6 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的? 2.5介绍了渲染流水线中的 DOM 生成、样式计算和布局三个阶段,2.6讲解渲染流水线后面的阶段…...

NO2.C++语言基础|C++和Java|常量|重载重写重定义|构造函数|强制转换|指针和引用|野指针和悬空指针|const修饰指针|函数指针(C++)
6. C 和 Java 区别(语⾔特性,垃圾回收,应⽤场景等) 指针: Java 语⾔让程序员没法找到指针来直接访问内存,没有指针的概念,并有内存的⾃动管理功能,从⽽有效的防⽌了 C 语⾔中的指针…...

【CSS】---- 纯 CSS 实现无限滚动轮播
1. 前言 仅使用 CSS 创建一个具有无限滚动轮播的动画,无需 JavaScript。首先是无限滚动轮播动画效果在我们常见的开发中都是借用 JavaScript 实现,如果纯粹使用 CSS,我觉得还是一个比较有趣的。 2. 效果预览 3. 效果分析 一屏展示了三个图片元素;动画依次向左移动;三个图…...

软考架构师笔记-计算机网络
1.9 计算机网络 OSI/RM 七层模型 物理层 二进制传输(中继器、集线器) (typedef) 数据链路层 传送以帧为单位的信息(网桥、交换机、网卡) 网络层 分组传输和路由选择(三层交换机、路由器)ARP/RARP/IGMP/ICMP/IP 传输层 端到端的连接(TCP/UDP)在前向纠错系统中,当接…...

Spring MVC 页面重定向返回后通过nginx代理 丢失端口号问题处理
Spring MVC页面重定向通过Nginx代理后出现端口丢失问题,通常由以下原因及解决方案构成: ## 一、Nginx配置问题(核心原因) 1. Host头传递不完整 Nginx默认未将原始请求的端口信息传递给后端,导致应用生成重定向…...

道可云人工智能每日资讯|亚马逊云业务部门成立智能体人工智能团队
道可云元宇宙每日简报(2025年3月6日)讯,今日元宇宙新鲜事有: 《杭州市富阳区未来产业培育行动计划(2025-2026年)》发布 3月3日,杭州市富阳区经信局正式发布了《杭州市富阳区未来产业培育行动计划(2025-2026年)》&…...

算力100问☞第72问:算力与算法、数据的关系是什么?
目录 1、数据是基础 2、算法是核心 3、算力是保障 4、三者的关系 5、实际应用中的体现 算力、算法和数据是人工智能和计算机科学领域的三个核心要素,它们之间相互依赖、相互促进,共同构成了现代计算系统的基础。以下是它们之间的关系: 1、数据是基础 定义:数据是信息…...
AI-Ollama本地大语言模型运行框架与Ollama javascript接入
1.Ollama Ollama 是一个开源的大型语言模型(LLM)平台,旨在让用户能够轻松地在本地运行、管理和与大型语言模型进行交互。 Ollama 提供了一个简单的方式来加载和使用各种预训练的语言模型,支持文本生成、翻译、代码编写、问答等多种…...

Java开发的AI应用框架简述——LangChain4j、Spring AI、Agent-Flex
LangChain4j LangChain4j官网 star很多,文档齐全,在AI服务中,提供了丰富的功能,示例代码丰富。 简介 是一个功能丰富、易于使用的Java AI开发框架,特别适合需要快速集成和使用大型语言模型的Java开发者。 项目特点 …...

【算法day2】无重复字符的最长子串 两数之和
无重复字符的最长子串 给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。 https://leetcode.cn/problems/longest-substring-without-repeating-characters/ class Solution { public:int lengthOfLongestSubstring(string s) {int sub_length …...

HarmonyOS:基于hmrouter实现Page的生命周期监听
前言:在使用ArkTs语言写鸿蒙的App中,我们发现Page的生命周期函数,如下: 页面的生命周期(32) onPageShow:页面显示触发(页面特有) onPageHide:页面隐藏触发(页面特有) onBackPress:当用户点击返回按钮时…...

DeepSeek + 飞书多维表格搭建你的高效工作流
众所周知,大模型DeepSeek擅长于处理大规模语言模型推理任务,特别是在成本降低和思维链推理方面表现出色,我们一般把大模型必做我们的大脑,但是一个人不能只有大脑,还需要其他输入输出以及操作支配的眼耳鼻嘴手足等。…...

uniapp+<script setup lang=“ts“>使用 uni.$emit和uni.$on全局传递数据
注意: 在A页面直接使用 uni.$emit(changeCategoryKey, childCategory)传递,在B页面使用 uni.$on(changeCategoryKey, (val) > {console.log(val, 取值);});只在组件传递有效,页面跳转后是无效的 跳转页面使用的传递数据的方法如下&…...

综合使用pandas、numpy、matplotlib、seaborn库做数据分析、挖掘、可视化项目
目录 1.结构化数据挖掘 1.1依赖库导入和数据读取 1.2各品牌机型及售价统计 1.3视频录制规格与价格关联性分析 2.结构化数据预处理 2.1筛选特征 2.2特征标签归一化及编码 1.结构化数据挖掘 1.1依赖库导入和数据读取 导入必要的依赖库,读取 csv 格式数据集转化为 Data…...

docker中kibana启动后,通过浏览器访问,出现server is not ready yet
问题:当我在浏览器访问kibana时,浏览器给我报了server is not ready yet. 在网上试了很多方法,都未能解决,下面是我的方法: 查看kibana日志: docker logs -f kibana从控制台打印的日志可以发现ÿ…...

十、Redis 主从复制:原理解析、配置实践与优化策略
Redis 主从复制:原理解析、配置实践与优化策略 Redis 作为高性能的 NoSQL 数据库,主从复制(Master-Slave Replication) 是其核心特性之一。主从复制用于数据冗余、读负载分担、故障恢复,是 Redis 构建高可用架构的基础。本文将深入解析 Redis 主从复制的配置方法、复制机…...

使用JMeter(组件详细介绍+使用方式及步骤)
JSON操作符 在我们使用请求时,经常会遇到JSON格式的请求体,所以在介绍组件之前我会将介绍部分操作符,在进行操作时是很重要的 Operator Description $ 表示根元素 当前元素 * 通配符,所有节点 .. 选择所有符合条件的节点 .name 子元素,name是子元素名称 [start:e…...

lamp平台的应用
一.lamp介绍 网站: 静态网站 动态网站 【php语言 .php结尾的文件】 作用:运行php语言编写的动态网站应用 lamp LinuxApache【负责解析静态资源】MySQL【负责存储网站产生的数据】PHP【负责解析动态资源】 如上图所示,是lamp平台中三…...

蓝桥杯4T平台(串口打印电压值)
知识点:串口(单片机发送数据)按键ADC 题目 配置 代码 adc.c uint16_t getadc2(void) {uint16_t adc0;HAL_ADC_Start(&hadc2);adcHAL_ADC_GetValue(&hadc2);return adc; } adc.h uint16_t getadc2(void); main.c #include "lcd.h" #include…...

使用ASIWebPageRequest库编写Objective-C下载器程序
使用 ASIWebPageRequest 库编写 Objective-C 下载器程序是一个简单且高效的方式来处理 HTTP 请求。在 ASIHTTPRequest 和 ASIWebPageRequest 中,ASIWebPageRequest 是专门用于下载网页及其资源的库。 1. 安装 ASIWebPageRequest 首先,你需要安装 ASIHT…...

代码随想录算法训练营 | 图论 | 孤岛总面积、沉没孤岛
101. 孤岛的总面积//思路大概是先计算面积,然后如果有接触路面就返回false。可能稍微多余算了太多无用面积。 #include<bits/stdc.h> using namespace std; void sum(vector<vector<bool>>& finded,const vector<vector<int>>&a…...

迷你世界脚本出生点接口:Spawnport
出生点接口:Spawnport 彼得兔 更新时间: 2023-04-26 10:19:56 具体函数名及描述如下: 序号 函数名 函数描述 1 getSpawnPoint(...) 获取默认出生点 2 setSpawnPoint(...) 设置出生点位置 3 getChunkValidSpawnPos(...) 获取区块有效刷新点…...

双链路提升网络传输的可靠性扩展可用带宽
为了提升网络传输的可靠性或增加网络可用带宽, 通常使用双链路冗余备份或者双链路聚合的方式。 本文介绍几种双链路网络通信的案例。 5GWiFi冗余传输 双Socket绑定不同网络接口:通过Android的ConnectivityManager绑定5G蜂窝网络和WiFi的Socket连接&…...

Pytest测试用例执行跳过的3种方式
文章目录 1.前言2.使用 pytest.mark.skip 标记无条件跳过3.使用 pytest.mark.skipif 标记根据条件跳过4. 执行pytest.skip()方法跳过测试用例 1.前言 在实际场景中,我们可能某条测试用例没写完,代码执行时会报错,或者是在一些条件下不让某些…...

【蓝桥杯】每天一题,理解逻辑(3/90)【Leetcode 快乐数】
闲话系列:每日一题,秃头有我,Hello!!!!!,我是IF‘Maxue,欢迎大佬们来参观我写的蓝桥杯系列,我好久没有更新博客了,因为up猪我寒假用自己的劳动换了…...

深度学习Save Best、Early Stop
一、Save Best 今天的大模型,在训练过程中可能会终止,但是模型其实是可以接着练的,假设GPU挂了,可以接着训练,在原有的权重上,训练其实就是更新w,如果前面对w进行了存档,那么可以从…...

数据库与存储优化
一、MySQL深度优化 索引优化 B树索引结构 结构特点: 平衡多路搜索树,所有数据存储在叶子节点,非叶子节点仅存键值和指针。叶子节点通过双向链表连接,支持范围查询高效遍历。 优势: 减少磁盘IO(高扇出&#…...

Android15请求动态申请存储权限完整示例
效果: 1.修改AndroidManifest.xml增加如下内容: <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-perm...

C/C++蓝桥杯算法真题打卡(Day3)
一、P8598 [蓝桥杯 2013 省 AB] 错误票据 - 洛谷 算法代码: #include<bits/stdc.h> using namespace std;int main() {int N;cin >> N; // 读取数据行数unordered_map<int, int> idCount; // 用于统计每个ID出现的次数vector<int> ids; …...

【数据结构与算法】Java描述:第二节:LinkedList 链表
一、链表的概念与结构 1.1 概念: 通俗的来说,链表是由一个个结点连接起来的就叫链表。 1.2 结构: 链表存储的数据 在 物理上是不一定连续的,它是由前面链接后面,一个个连起来的。 二、Java底层的 LinkedList 2.1…...