HTML页面渲染过程
前言
文章很长,凡是我觉得好的东西统统都塞进来了。看了很多的文章,有些说法甚至都不统一,所以还动用了AI搜索。总之希望这篇文章能有点用,如有错误,欢迎指正。
浏览器介绍
浏览器的主要组件包括:
- 界面:包括地址栏、后退/前进按钮、书签菜单等。浏览器界面的每个部分,但显示请求网页的窗口除外。
- 浏览器引擎:在界面和渲染引擎之间协调操作。
- 渲染引擎:负责显示请求的内容。例如,如果请求的内容是 HTML,则呈现引擎会解析 HTML 和 CSS,并在屏幕上显示解析的内容。
- 网络:对于 HTTP 请求等网络调用,在平台无关接口后面为不同平台使用不同的实现。
- 界面后端:用于绘制组合框和窗口等基本 widget。此后端公开的接口并非平台专用。在底层,它使用操作系统界面方法。
- JavaScript 解释器。用于解析和执行 JavaScript 代码。
- 数据存储:这是持久层。浏览器可能需要在本地保存各种数据,例如 Cookie。浏览器还支持 localStorage、IndexedDB、WebSQL 和 FileSystem 等存储机制。
注意:Chrome 等浏览器会运行多个渲染引擎实例,每个标签页一个,每个标签页都在单独的进程中运行。
渲染引擎
不同的浏览器使用不同的渲染引擎:Internet Explorer 使用 Trident、Firefox 使用 Gecko、Safari 使用 WebKit。Chrome 和 Opera(从 15 版开始)使用 Blink,即 WebKit 的一个分支。
WebKit 是一个开源渲染引擎,最初是 Linux 平台的引擎,后来被 Apple 修改为支持 Mac 和 Windows。
渲染流程
渲染引擎将开始从网络层获取请求的文档内容,通常是分块( 8KB)接收的。之后,渲染引擎的基本流程如下:
现代浏览器的渲染过程,最简单的描述就是解析-构建-布局-绘制。它会经历以下几个主要阶段:
- 解析 HTML 构建 DOM 树:
- 浏览器从上到下解析 HTML 文档,创建文档对象模型(DOM)树。DOM 树表示了页面的结构和层次关系。
- 加载外部资源:
- 在解析 HTML 过程中,浏览器会发现对 CSS 文件、JavaScript 文件、图片等外部资源的引用。浏览器会发起额外的请求来获取这些资源。
- 解析 CSS 构建 CSSOM:
- 浏览器解析加载的 CSS 文件,构建 CSS 对象模型(CSSOM)。CSSOM 包含了页面中各个元素的样式信息。
- 构建渲染树:
- 渲染树是 DOM 树和 CSSOM 的结合,它包含了所有需要渲染的页面元素及其样式信息。这个过程决定了每个元素在页面上的位置和外观。
- 布局(重排):
- 浏览器根据渲染树计算每个元素的几何信息(如位置、大小),这个过程称为布局或重排。
- 绘制(重绘):
- 浏览器将渲染树中的元素绘制到屏幕上,包括填充背景色、绘制文本、边框、图片等。
更具体一点的流程是:
- 解析阶段:
- 解析 HTML,生成 DOM 树。
- 解析 CSS,生成 CSSOM 树。
- 合并 DOM 树和 CSSOM 树,生成渲染树。
- 布局阶段:
- 根据渲染树计算每个元素的几何信息(位置和大小)。
- 分层阶段:
- 将渲染树分成多个图层,生成图层树(Layer Tree)。
- 绘制阶段:
- 绘制每个图层的内容,生成绘制指令。
- 光栅化阶段:
- 将绘制指令转换为位图(Rasterization),通常由 GPU 加速完成。
- 合成阶段:
- 合成线程将所有图层的图块组合成最终的屏幕图像。
- 显示阶段:
- 将合成后的图像呈现到屏幕上。
以上是一个渐进的过程,为了提供更好的用户体验,渲染引擎会尝试尽快在屏幕上显示内容。它不会等到所有 HTML 都解析完毕后才开始构建和布局渲染树。系统会解析并显示部分内容,同时继续处理不断从网络传入的其余内容。
WebKit主要流程示例:
解析
浏览器的渲染流程从解析开始,解析是将 HTML 和 CSS 转换为浏览器可以理解的内部数据结构的过程。解析阶段是整个渲染流程的基础,直接影响后续的布局、绘制和显示。
解析基础:
解析文档意味着将其转换为代码可以使用的结构。解析结果通常是表示文档结构的节点树。这称为解析树或语法树。
例如,解析表达式 2 + 3 - 1 可能会返回以下树:
语法
解析基于文档遵循的语法规则:文档所用的语言或格式。您可以解析的每种格式都必须具有由词汇和语法规则组成的确定性语法。这种语言称为无上下文语法。
解析器 - 词法分析器组合
解析可以分为两个子过程:词法分析和语法分析。
词法分析是将输入内容拆分为词元的流程。令牌是语言词汇:一系列有效的构建块。用人类语言来说,它包含该语言字典中显示的所有字词。
语法分析是指应用语言语法规则。
解析器通常会将工作分为两个部分:负责将输入拆分为有效令牌的词法分析器(有时称为“分词器”),以及负责根据语言语法规则分析文档结构以构建解析树的解析器。
解析器知道如何移除空格和换行符等无关紧要的字符。
解析过程是迭代的。解析器通常会向词法分析器请求新的令牌,并尝试将令牌与某个语法规则进行匹配。如果匹配到规则,系统会将与令牌对应的节点添加到解析树中,解析器会请求另一个令牌。
如果没有匹配的规则,解析器将在内部存储令牌,并不断请求令牌,直到找到与内部存储的所有令牌匹配的规则。如果未找到任何规则,解析器将引发异常。这意味着该文档无效且包含语法错误。
在许多情况下,解析树不是最终产品。解析通常用于翻译:将输入文档转换为其他格式。例如编译。将源代码编译为机器代码的编译器会先将其解析为解析树,然后将该树转换为机器代码文档。
解析器类型
解析器有两种类型:自上而下(LL)解析器和自下而上(LR)解析器(又被称“移位-规约”解析器)。
- 自上而下的解析器会检查语法的宏观结构,并尝试找到匹配的规则。按照从左到右的顺序扫描输入,并从左推导生成语法树。它适用于上下文无关文法,通常使用递归下降的方式实现。
示例:
代码实现:E -> T E' // 含义:E 是一个表达式,由一个 T(项)和一个 E'(表达式的后续部分)组成 E' -> + T E' | ε // 含义:E' 是表达式的后续部分,可以是:一个加号 +,后面跟一个 T(项)和另一个 E'(递归定义);或者是空(ε 表示空产生式,表示没有更多的内容) T -> int // 含义:T 是一个项,表示一个整数。
class LLParser {constructor(tokens) {this.tokens = tokens; // 输入的标记列表this.pos = 0; // 当前标记的位置}parse() {return this.E(); // 从非终结符 E 开始解析}E() {// E -> T E'const node = { type: "E", children: [] };node.children.push(this.T());node.children.push(this.EPrime());return node;}EPrime() {// E' -> + T E' | εif (this.match("+")) {const node = { type: "E'", children: [] };this.consume("+");node.children.push(this.T());node.children.push(this.EPrime());return node;} else {return { type: "E'", children: ["ε"] }; // 空产生式}}T() {// T -> intif (this.match("int")) {const node = { type: "T", value: this.consume("int") };return node;} else {throw new Error("Expected 'int'");}}match(tokenType) {// 检查当前标记是否匹配return this.pos < this.tokens.length && this.tokens[this.pos] === tokenType;}consume(tokenType) {// 消耗当前标记并移动到下一个if (this.match(tokenType)) {const token = this.tokens[this.pos];this.pos++;return token;} else {throw new Error(`Expected ${tokenType}`);}} }// 示例输入 const tokens = ["int", "+", "int", "+", "int"]; const parser = new LLParser(tokens); const syntaxTree = parser.parse(); console.log(JSON.stringify(syntaxTree, null, 2)); /* 输出:{"type": "E","children": [{"type": "T","value": "int"},{"type": "E'","children": [{"type": "T","value": "int"},{"type": "E'","children": [{"type": "T","value": "int"},{"type": "E'","children": ["ε"]}]}]}] } */
- 自下而上解析器从输入开始,从低级规则开始逐步转换为语法规则,直到满足高级规则。按照从左到右的顺序扫描输入,并从右推导生成语法树。它适用于更复杂的上下文无关文法,通常使用移入-归约的方式实现。
示例:
代码实现:E -> E + T | T T -> int
// LR(0)分析器class LRParser {constructor(tokens) {// 初始化输入 token 流(末尾加 $ 结束符)this.tokens = [...tokens, "$"];// 初始化状态栈 stack,初始状态为 0this.stack = [0];// 初始化语法树节点栈 treeStackthis.pos = 0;// 初始化当前读取位置 posthis.treeStack = [];}getAction(state, token) {const actionTable = {"0,int": { action: "shift", to: 2 },"2,+": { action: "shift", to: 3 },"2,$": { action: "reduce", prod: "E -> int" },"1,+": { action: "shift", to: 4 },"1,$": { action: "accept" },"3,int": { action: "shift", to: 5 },"4,int": { action: "shift", to: 5 },"5,+": { action: "reduce", prod: "E -> int" },"5,$": { action: "reduce", prod: "E -> int" },"4,$": { action: "reduce", prod: "E -> E + E" },"4,+": { action: "reduce", prod: "E -> E + E" },};return actionTable[`${state},${token}`] || { action: "error" };}getGoto(state, symbol) {const gotoTable = {"0,E": 1,"3,E": 4,"4,E": 4,};return gotoTable[`${state},${symbol}`];}getProductions(prod) {const productions = {"E -> int": { rhsLen: 1, build: "E -> int" },"E -> E + E": { rhsLen: 3, build: "E -> E + E" }};return productions[prod];}// 移进操作:将当前 token 和目标状态压入栈,并为 token 创建语法树叶子节点,推进输入指针shift(token, to) {this.stack.push(token);this.stack.push(to);// 构造终结符叶子节点this.treeStack.push({ type: token, value: token });this.pos++;}// 规约操作:根据产生式弹出相应的栈元素,将非终结符压入栈,并调用 buildTree 构建语法树节点,然后查 GOTO 表压入新状态reduce(prod) {const p = this.getProductions(prod);if (!p) throw new Error("未知产生式: " + prod);for (let i = 0; i < p.rhsLen * 2; i++) this.stack.pop();const prevState = this.stack[this.stack.length - 1];this.stack.push("E");this.buildTree(p.build);const goto = this.getGoto(prevState, "E");if (goto !== undefined) {this.stack.push(goto);} else {throw new Error("GOTO错误");}}// 语法树构建buildTree(prod) {if (prod === "E -> int") {const intNode = this.treeStack.pop();this.treeStack.push({ type: "E", children: [intNode] });} else if (prod === "E -> E + E") {const right = this.treeStack.pop();const plus = this.treeStack.pop();const left = this.treeStack.pop();this.treeStack.push({ type: "E", children: [left, plus, right] });}}// 解析函数:循环读取 token,查 action 表,执行相应操作parse() {const maxSteps = 100;for (let step = 0; step < maxSteps; step++) {const state = this.stack[this.stack.length - 1];const token = this.tokens[this.pos];const act = this.getAction(state, token);if (act.action === "shift") {this.shift(token, act.to);} else if (act.action === "reduce") {this.reduce(act.prod);} else if (act.action === "accept") {console.log("解析成功!");return this.treeStack[0];} else {throw new Error("解析失败");}}throw new Error("解析步骤过多,可能死循环");}}// 示例输入const tokens = ["int", "+", "int"];const parser = new LRParser(tokens);const syntaxTree = parser.parse();console.log(JSON.stringify(syntaxTree, null, 2)); // 输出: /*解析成功!{"type": "E","children": [{"type": "int","value": "int"},{"type": "+","value": "+"},{"type": "E","children": [{"type": "int","value": "int"}]}]}*/
LL 和 LR 的对比:
特性 | LL解析器 | LR解析器 |
---|---|---|
解析方式 | 自顶向下 | 自底向上 |
适用文法 | 简单的上下文无关文法 | 更复杂的上下文无关文法 |
实现难度 | 相对简单 | 相对复杂 |
解析过程 | 递归下降或预测分析 | 移入-归约或状态机 |
常见实现 | 手写递归下降解析器 | 自动生成的 LR 解析器(如 YACC) |
WebKit 使用两个众所周知的解析器生成器:Flex 用于创建词法分析器,Bison 用于创建解析器(您可能会遇到名称为 Lex 和 Yacc 的解析器)。Flex 输入是包含令牌正则表达式定义的文件。Bison 的输入是 BNF 格式的语言语法规则。
html解析
定义 HTML 的正式格式是 DTD(文档类型定义),它不是无上下文语法。HTML 无法轻松地通过解析器所需的无上下文语法进行定义,上面提到的解析方式(LL和LR)都不适用于它。浏览器会创建自定义解析器来解析 HTML,解析算法一般包含两个阶段:令牌化和树构建。
词元化是词法分析,用于将输入解析为词元。HTML 令牌包括起始标记、结束标记、属性名称和属性值。
分词器会识别令牌,将其传递给树构造函数,并使用下一个字符来识别下一个令牌,以此类推,直到输入结束。
令牌化算法的输出是 HTML 令牌。 该算法以状态机的形式表示。每个状态都会使用输入流中的一个或多个字符,并根据这些字符更新下一个状态。此决策会受到当前的令牌化状态和树构建状态的影响。这意味着,对于正确的下一个状态,相同的已消耗字符会产生不同的结果,具体取决于当前状态。 该算法过于复杂,无法完整描述,因此我们来看一个简单的示例,以便了解其原理。
基本示例 - 对以下 HTML 进行令牌化:
<html><body>Hello world</body>
</html>
初始状态为“数据状态”。 遇到 < 字符时,状态会更改为“标记处于打开状态”。使用 a-z 字符会导致创建“开始标记令牌”,状态会更改为“标记名称状态”。我们会一直保持此状态,直到 > 字符被消耗完为止。每个字符都会附加到新令牌名称后面。在本例中,创建的令牌是 html 令牌。
达到 > 标记后,系统会发出当前令牌,并且状态会恢复为“数据状态”。系统会按照相同的步骤处理 标记。到目前为止,系统已发出 html 和 body 标记。现在,我们返回到“数据状态”。 使用 Hello world 的 H 字符会导致创建并发送字符令牌,此过程会持续到达到 的 <。我们将为 Hello world 的每个字符发出一个字符令牌。
现在,我们回到“代码处于打开状态”。 使用下一个输入 / 会导致创建 end tag token 并移至“标记名称状态”。再次强调一下,我们会一直保持此状态,直到达到 >。然后,系统会发出新的代码令牌,我们会返回到“数据状态”。 系统会将 输入视为前面的示例。
构建DOM树
DOM构建是增量的。
处理 HTML 标记并构造 DOM 树。HTML
解析包括标记化(令牌化)和树结构构建。单个 DOM
节点以起始标签标记(startTag)开始,以结束标签标记(endTag)结束。节点包含有关 HTML
元素的(使用标记描述的)所有相关信息。节点根据标记的层次结构连接到 DOM
树中。如果一组起始标签标记和一组结束标签标记之间又有一组起始标签标记和一组结束标签标记,那么就会出现节点内有节点的情况,这就是我们定义 DOM
树层次结构的方法。HTML 标记包括开始和结束标记,以及属性名和值。如果文档格式良好,则解析它会简单而快速。解析器将标记化的输入解析到文档中,构建文档树。
创建解析器时,系统会创建 Document
对象。在树构建阶段,系统会修改根目录中包含文档的 DOM 树,并向其中添加元素。分词器发出的每个节点都将由树构造函数处理。对于每个令牌,规范会定义哪些 DOM 元素与其相关,以及将为此令牌创建哪些 DOM 元素。该元素会添加到 DOM 树和打开的元素堆栈(此堆栈用于更正嵌套不匹配和未闭合标记)中。 该算法还可描述为状态机。这些状态称为“插入模式”。
我们来看看示例输入的树构建过程:
<html><body>Hello world</body>
</html>
树构建阶段的输入是来自令牌化阶段的一系列令牌。第一种模式是“初始模式”。收到“html”令牌将导致系统切换到“html 之前”模式,并在该模式下重新处理令牌。这将导致创建 HTMLHtmlElement 元素,该元素将附加到根 Document 对象。
状态将更改为“在 head 之前”。然后,系统会收到“body”令牌。系统会隐式创建 HTMLHeadElement,即使我们没有“head”令牌,它也会被添加到树中。
现在,我们将进入“在头部前面”模式,然后进入“在头部后面”模式。系统会重新处理正文令牌,创建并插入 HTMLBodyElement,并将模式转换为“in body”。
现在,系统会收到“Hello world”字符串的字符令牌。第一个字符会导致创建并插入“文本”节点,其他字符会附加到该节点。
收到正文结束令牌后,系统会转换为“正文后”模式。现在,我们将收到 html 结束标记,这会将我们转换到“body 后”模式。收到文件结束令牌后,解析将结束。
在此阶段,浏览器会将文档标记为交互式,并开始解析处于“延迟”模式的脚本:这些脚本应在文档解析完毕后执行。然后,文档状态将设为“complete”,并触发“load”事件。
总结:
DOM 树描述了文档的内容。<html>
元素是第一个标签也是文档树的根节点。树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点。DOM 节点的数量越多,构建 DOM 树所需的时间就越长。
当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析。当遇到一个 CSS 文件时,解析也可以继续进行,但是对于 <script>
标签(特别是没有 async
或者 defer
属性的)会阻塞渲染并停止 HTML 的解析。尽管浏览器的预加载扫描器加速了这个过程,但过多的脚本仍然是一个重要的瓶颈。
DOM树构建过程主要涉及以下步骤:(简单来说就是:字节 → 字符 → 标记化 → 节点 → 对象模型)
- 字节转换字符:浏览器通过网络进程从服务器接收 HTML 文件,接收到的数据是字节流(
Byte Stream
)。再根据HTTP 响应头中的Content-Type
和charset
指定的编码方式(如 UTF-8、ISO-8859-1 等),将字节流解码为字符流(Character Stream
)(如果没有指定编码,浏览器会尝试自动检测编码方式)。 - 词法分析:浏览器的 HTML 解析器会对字符流进行词法分析,将其分解为一个个标记(Token),如标签
<div>
、属性class="example"
和文本节点。解析器会识别这些标记的类型(如开始标签、结束标签、文本等)。标记是 HTML 的基本组成部分,包括:- 开始标签(Start Tag): 如
<div>
- 结束标签(End Tag): 如
</div>
- 自闭合标签(Self-closing Tag): 如
<img />
- 文本节点(Text Node): 如
Hello World
- 注释(Comment): 如
<!-- This is a comment -->
- 开始标签(Start Tag): 如
- 标记化(Tokenization):HTML 解析器会根据 HTML 规范,将字符流转换为标记流。例如:
<div class="example">Hello</div>
会被标记为:<div>
(开始标签)class="example"
(属性)Hello
(文本节点)</div>
(结束标签)
- 语法分析(Syntax Analysis):浏览器根据 HTML 的语法规则,将标记流解析为节点(Node)。节点是 DOM 树的基本单位,包括:
- 元素节点(Element Node): 对应 HTML 标签。
- 文本节点(Text Node): 对应 HTML 中的文本内容。
- 属性节点(Attribute Node): 对应 HTML 标签的属性。
- 构建 DOM 树:浏览器会根据标记的嵌套关系,将节点组织成一棵树状结构,即 DOM 树。例如:
会生成如下dom树:<div><p>Hello</p> </div>
div
└── p
└── “Hello” - 增量构建:HTML 是流式解析的,浏览器在接收到部分 HTML 内容时就会开始解析并构建 DOM 树,而不是等待整个文件下载完成。这使得页面可以逐步呈现,提高用户体验。
整个过程的最终输出是简单网页的文档对象模型 (DOM
),浏览器会使用该模型对网页进行所有后续处理。 下图为流程示例:
每次浏览器处理 HTML
标记时,都会完成之前定义的所有步骤:将字节转换为字符、识别令牌、将令牌转换为节点,以及构建 DOM
树。整个过程可能需要一些时间,尤其是在我们有大量 HTML
需要处理时。
预加载扫描器(preload scanner)
浏览器构建 DOM
树时,这个过程占用了主线程。同时,预加载扫描器会解析可用的内容并请求高优先级的资源,如 CSS
、JavaScript
和 web
字体。因为有了预加载扫描器,我们不必等到解析器找到对外部资源的引用时才去请求。它将在后台检索资源,而当主 HTML
解析器解析到要请求的资源时,它们可能已经下载中了,或者已经被下载。预加载扫描器提供的优化减少了阻塞。
<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="图像描述" />
<script src="anotherscript.js" async></script>
在这个例子中,当主线程在解析 HTML
和 CSS
时,预加载扫描器将找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,当 JavaScript
解析和执行顺序不重要时,可以添加 async
属性或 defer
属性。
等待获取 CSS
不会阻塞 HTML
的解析或者下载,但是它确实会阻塞 JavaScript
,因为 JavaScript
经常用于查询元素的 CSS
属性。
构建 CSSOM 树
处理 CSS
并构建 CSSOM 树。CSS
对象模型和 DOM
是相似的。DOM
和 CSSOM
是两棵树。它们是独立的数据结构。浏览器将 CSS
规则转换为可以理解和使用的样式映射。浏览器遍历 CSS
中的每个规则集,根据 CSS
选择器创建具有父、子和兄弟关系的节点树。
与 HTML
类似,浏览器需要将接收到的 CSS
规则转换为可处理的格式。因此,它重复了 HTML
到对象的过程,但这次是针对 CSS
。CSS
字节会被转换为字符,然后被转换为令牌,然后被转换为节点,最后它们会关联到一个被称为“CSS
对象模型”(CSSOM) 的树结构:
CSSOM
树包括来自用户代理样式表的样式。浏览器从适用于节点的最通用规则开始(例如 body 的子元素可以应用所有 body 样式),然后通过应用更具体的规则递归地优化计算的样式。换句话说,它级联属性值(级联指的是多个样式规则同时作用于同一个元素时,根据优先级和特定的规则来确定最终应用的样式)。
其他过程
JavaScript
编译
在解析CSS
和创建CSSOM
的同时,包括JavaScript
文件在内的其他资源也在下载(这要归功于预加载扫描器)。JavaScript
会被解析、编译和解释。脚本被解析为抽象语法树。有些浏览器引擎会将抽象语法树输入编译器,输出字节码。这就是所谓的JavaScript
编译。大部分代码都是在主线程上解释的,但也有例外,例如在 web worker 中运行的代码。- 构建无障碍树
浏览器还构建辅助设备用于分析和解释内容的无障碍树。无障碍对象模型(AOM
)类似于DOM
的语义版本。当DOM
更新时,浏览器会更新辅助功能树。辅助技术本身无法修改无障碍树。
在构建AOM
之前,屏幕阅读器无法访问内容。
构建渲染树
在解析步骤中创建的 CSSOM
树和 DOM
树组合成一个渲染树(Firefox 将渲染树中的元素称为“帧”,WebKit 使用“渲染程序”或“渲染对象”一词),然后用于计算每个可见元素的布局,最后将其绘制到屏幕上。在某些情况下,可以将内容提升到它们自己的层并进行合成,通过在 GPU 而不是 CPU 上绘制屏幕的一部分来提高性能,从而释放主线程。
为构建渲染树,浏览器大致执行以下操作:
- 从 DOM 树的根节点开始遍历每个可见节点。
- 非可视 DOM 元素不会插入到渲染树中(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
- 某些节点使用 CSS 隐藏,因此在渲染树中也会被忽略,显示值被分配为“none”的元素不会显示在树中(而“hidden”可见性的元素会显示在树中)。
- 对于每个可见节点,为其找到适配的
CSSOM
规则并应用它们。渲染树包含所有可见节点的内容和计算样式,将所有相关样式与 DOM 树中的每个可见节点匹配起来,并根据CSS
级联,确定每个节点的计算样式。 - 发出带有内容及其计算样式的可见节点
布局(Layout)
渲染树构建完毕后,浏览器就开始布局(布局有时也称为“自动重排”或重新流布局。它是是查找元素几何图形的过程,主线程会遍历 DOM 和计算样式,并创建包含 x、y 坐标和边界框大小等信息的布局树)。
渲染树标识了哪些节点会显示(即使不可见)及其计算样式,但不标识每个节点的尺寸或位置。为了确定每个对象的确切大小和位置,浏览器会从渲染树的根开始遍历。
第一次确定每个节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为重排。
布局是一个递归过程。它从根渲染程序开始,该渲染程序对应于 HTML 文档的 <html>
元素。布局会继续递归地遍历部分或全部帧层次结构,为需要的每个渲染程序计算几何信息。
根渲染程序的位置为 0,0,其尺寸为视口(浏览器窗口的可见部分)。
所有渲染程序都具有“布局”或“重新布局”方法,每个渲染程序都会调用其需要布局的子项的布局方法。
布局是一个相对耗时的操作,特别是在处理大型复杂页面时。为了提高性能,浏览器会使用一些优化策略,例如增量更新、异步布局等。同时,开发者也可以通过优化CSS样式、减少DOM操作和避免频繁修改尺寸等方式来提升页面渲染性能。
脏位系统
为了避免对每项细微更改都进行完整布局,浏览器使用“脏位”系统。更改或添加的渲染程序会将自身及其子项标记为“脏”:需要布局。
有两个标记:“脏”和“子项脏”,这意味着,虽然渲染程序本身可能没问题,但它至少有一个子项需要布局。
全局布局和增量布局
布局可在整个渲染树上触发 - 这是“全局”布局。造成这种情况的原因可能是:
- 会影响所有渲染程序的全局样式更改,例如字号更改。
- 由于屏幕大小调整
布局可以是增量布局,只有脏渲染程序才会进行布局(这可能会导致一些损坏,需要额外的布局)。
当渲染程序脏时,系统会触发增量布局(异步)。例如,当额外内容从网络传入并添加到 DOM 树后,将新的渲染程序附加到渲染树中时。增量布局 - 仅排列脏渲染程序及其子项示例图:
异步布局和同步布局
增量布局是异步完成的。Firefox 会为增量布局队列“重新流式传输命令”,并且调度程序会触发这些命令的批量执行。WebKit 还有一个用于执行增量布局的计时器,它会遍历树并布局“脏”渲染程序。
请求样式信息(例如“offsetHeight”)的脚本可以同步触发增量布局。
全局布局通常会同步触发。
有时,由于某些属性(例如滚动位置)发生变化,布局会在初始布局后作为回调触发。
渲染树(render tree)和布局树(layout tree)的区别:
特性 | 渲染树 | 布局树 |
---|---|---|
定义 | 描述页面可见元素及其样式 | 定义每个可见元素的几何位置和尺寸 |
构建过程 | 结合 DOM 树和 CSSOM 树,过滤掉不可见元素 | 基于渲染树,进行几何计算 |
包含的节点 | 包含所有可见的 DOM 元素 | 包含需要进行几何位置和尺寸计算的元素 |
作用 | 计算元素的样式和可见性 | 计算元素的确切位置和尺寸 |
性能影响 | 构建渲染树会受 CSS 的影响,复杂的样式计算会影响性能 | 布局过程(重排)通常比样式计算更耗时,因为它涉及几何计算 |
核心区别:
对比维度 | 渲染树(Render Tree) | 布局树(Layout Tree) |
---|---|---|
作用阶段 | 样式计算后,布局前 | 布局阶段 |
内容组成 | 可见节点 计算样式 | 节点几何信息(位置、尺寸) |
动态性 | 样式变化时重建 | 几何属性变化时重建 |
常见误解澄清 :
-
误区:”渲染树即布局树“
正解:渲染树是布局树的输入,布局树是渲染树的几何扩展。两者在流程中串联,但功能分离。 -
误区:”所有样式变化都会触发布局“
正解:仅几何属性(如宽度、位置)触发布局;颜色、透明度等仅触发绘制(Paint)。
总结:
渲染树负责整合DOM和CSSOM的可视化信息,而布局树专注于几何计算。两者共同构成浏览器渲染的核心流程,但职责分明。理解其差异有助于优化网页性能(如减少不必要的布局操作)。
分层(Layer)
分层是渲染过程中非常重要的一步,它决定了页面的哪些部分需要单独处理。将内容提升到 GPU 上的层(而不是 CPU 上的主线程)可以提高绘制和重新绘制性能,还可以减少不必要的重绘和布局计算,并允许浏览器对每个图层进行独立处理和优化。
浏览器通过以下规则决定是否为元素创建独立图层:
条件 | 示例 | 底层逻辑 |
---|---|---|
层叠上下文(Stacking Context) | position: fixed 、position: sticky 、position: absolute; 、z-index 、opacity < 1 | 确保层叠顺序,避免与其他元素混合计算。 |
需裁剪(Clip)的区域 | 内容溢出容器(如长文本超出overflow: hidden 的div ) | 独立处理裁剪部分,避免污染其他区域。 |
硬件加速需求 | transform: translateZ(0) 、will-change: transform | 强制提升至GPU层,绕过主线程计算。 |
视频/Canvas元素 | Flash、<video> 、<canvas> | 原生需独立解码或绘制,默认分层。 |
示例:
若一个元素设置position: fixed
,浏览器会为其创建独立图层,滚动时仅移动图层位置,无需重绘内容。
分层的好处:
- 独立更新:分层后,当某个图层的内容发生变化时,浏览器只需重新合成该图层,而不需要重新渲染整个页面。
- 硬件加速:一些图层可以利用 GPU 进行加速渲染,提高性能。
- 简化重排:分层减少了重排的范围,因为每个图层可以独立处理其内部的布局变化。
分层的缺点:每个图层占用额外内存(尤其是大尺寸图层),过度分层可能导致内存瓶颈。
总结:
阶段 | 分层(Layer) |
---|---|
核心目标 | 隔离变化区域,减少重绘范围 |
性能影响 | 图层数量影响内存占用 |
优化手段 | 按需提升、合并冗余图层 |
触发条件 | 层叠上下文、裁剪、硬件加速声明 |
分层的结果是生成一棵 图层树(Layer Tree),它与渲染树类似,但只包含需要单独处理的图层信息。
生成绘制指令(Painting Commands)
分层完成后,主线程为每个图层生成绘制指令列表(Paint Instructions)。绘制指令是描述如何绘制每个图层内容的操作序列,包含了绘制图形、文本、图像、边框、阴影等的具体步骤。它是光栅化(Rasterization)的输入,被存储在绘制队列中,供后续的光栅化阶段使用。通过生成绘制指令,浏览器可以将页面内容转换为 GPU 或 CPU 可处理的格式。例如:
// 示例绘制指令
[{ type: 'rect', x: 0, y: 0, width: 100, height: 100, color: '#ff0000' },{ type: 'text', x: 10, y: 50, content: 'Hello', font: '14px Arial' }
]
关键特点:
- 非像素操作:仅记录绘制命令,实际光栅化由合成线程完成。
- 层级隔离:各图层的绘制指令互不影响,并行处理。
绘制指令的生成过程:
-
遍历图层树:
- 浏览器遍历每个图层,确定需要绘制的内容
- 例如:背景颜色、文本、边框、图像等
-
生成绘制列表:
- 每个图层的内容被描述为一系列绘制操作,存储在绘制列表中
- 例如:
- 绘制矩形:
drawRect(x, y, width, height, color)
- 绘制文本:
drawText(x, y, font, text, color)
- 绘制图像:
drawImage(x, y, width, height, image)
- 绘制矩形:
-
优化绘制顺序:
- 浏览器会对绘制指令进行优化,减少不必要的绘制操作。
- 例如:合并相邻的绘制区域,避免重复绘制。
优化绘制指令的生成:
- 减少复杂的绘制操作:
- 避免使用复杂的阴影、渐变和剪裁效果。
- 使用独立图层:
- 对频繁变化的元素(如动画)使用
transform
或will-change
,将其提升为独立图层。
- 对频繁变化的元素(如动画)使用
- 避免不必要的重绘:
- 使用
opacity
和transform
创建动画,而不是直接修改布局属性(如width
和height
)。
- 使用
在生成绘制指令后,渲染主线程的工作基本完成,后续的工作由其他线程(如合成线程和光栅化线程)完成:
1. 合成线程(Compositor Thread)
- 职责:
- 合成线程接收渲染主线程生成的绘制指令。
- 将每个图层分割成多个图块(Tiles)。
- 确定哪些图块需要绘制(通常是当前屏幕可见区域的图块)。
- 将绘制任务交给光栅化线程。
- 优势:
- 合成线程独立于渲染主线程运行,可以在主线程忙于处理 JavaScript 或布局时继续工作。
- 这使得滚动和动画更加流畅。
2. 光栅化线程(Rasterization Thread)
- 职责:
- 光栅化线程将合成线程分割的图块转换为位图(Bitmap)。
- 光栅化通常由 GPU 加速完成,以提高性能。
3. GPU 合成和显示
- 职责:
- GPU 将光栅化生成的位图合成到屏幕上。
- 合成线程负责将所有图层的图块组合成最终的屏幕图像。
浏览器页面渲染过程可以总结为:
渲染主线程负责解析
HTML
和CSS
,生成渲染树,计算布局,分层,并为每个图层生成绘制指令集。生成绘制指令后,合成线程接管工作,将图层分块并交由光栅化线程进行光栅化,最终由 GPU 合成图像并显示到屏幕上。
光栅化(Rasterization)
在现代浏览器的渲染过程中,分块(Tiling) 和 光栅化(Rasterization) 是优化绘制性能的重要步骤。这两个过程主要发生在 绘制(Painting) 和 合成(Compositing) 阶段,用于将页面内容高效地转换为屏幕上的像素。
分块(Tiling)
分块是将页面的每个图层(Layer)分割成多个小的矩形区域(称为“图块”或“Tile”)。这些图块通常是固定大小的,例如 256x256 像素。
分块原因:
- 提高渲染效率:
- 页面可能包含大量内容,直接处理整个图层的绘制会非常耗时。
- 分块允许浏览器只处理当前屏幕可见区域的图块,而无需绘制整个页面。
- 支持增量更新:
- 当页面内容发生变化时,只需要重新绘制受影响的图块,而不是整个图层。
- 优化内存使用:
- 分块可以将大图层分解为小块,便于在内存中管理和缓存。
分块的过程:
- 图层分割:
- 每个图层被分割成多个固定大小的图块。
- 图块的大小通常由浏览器决定(例如 256x256 像素),以平衡性能和内存使用。
- 图块管理:
- 浏览器会优先处理当前屏幕可见区域的图块。
- 不可见区域的图块可能会被延迟处理,甚至被丢弃以节省内存。
分块的优势:
- 按需绘制: 只绘制当前屏幕可见的图块,减少不必要的绘制工作。
- 并行处理: 图块可以在多个线程或 GPU 上并行处理,提高渲染速度。
- 滚动优化: 当用户滚动页面时,只需要加载新的图块,而无需重新绘制整个页面。
光栅化(Rasterization)
光栅化是将矢量图形(如 HTML 元素、CSS 样式、SVG 等)转换为位图(Bitmap)的过程。位图是由像素组成的图像,最终会被显示在屏幕上。转换过程通过线程池 + GPU加速优化:
+-------------------+ +-------------------+
| 图层A | | 图层B |
| - 图块1 (视口内) | | - 图块1 |
| - 图块2 | | - 图块2 (视口内) |
+-------------------+ +-------------------+↓ ↓
+-------------------------------+
| GPU光栅化队列 |
| - 优先处理视口内图块 |
+-------------------------------+
光栅化原因:
- 屏幕只能显示像素,而 HTML 和 CSS 描述的是矢量图形。
- 光栅化将矢量图形转换为屏幕可显示的像素数据。
光栅化的过程:
- 绘制指令生成:
- 在绘制阶段,浏览器生成绘制指令(Painting Commands),描述如何绘制每个图块的内容。
- 例如:绘制矩形、文本、图像、边框等。
- 光栅化执行:
- 光栅化线程(通常是 GPU)根据绘制指令,将每个图块转换为位图。
- 位图是一个二维像素数组,每个像素包含颜色和透明度信息。
- 存储和传输:
- 光栅化后的位图会被存储在 GPU 的内存中,供合成线程使用。
优化策略:
- 视口优先:优先光栅化用户可见区域(Viewport)的图块。
- 增量更新:仅重绘发生变化的图块,而非整个图层。
合成与显示
在浏览器的渲染过程中,光栅化(Rasterization) 将矢量图形转换为位图后,接下来的步骤是 合成(Compositing) 和 显示(Display)。这些步骤由 合成线程 和 GPU 协同完成,最终将页面内容呈现到屏幕上。下面是一个简单的合成示例:
+----------------+ +----------------+ +----------------+
| 背景图层 | | 内容图层 | | 浮动层 |
| (z-index: 0) | | (z-index: 1) | | (z-index: 2) |
+----------------+ +----------------+ +----------------+↓ ↓ ↓
+-----------------------------------------------------+
| 最终合成帧(按z-index顺序叠加) |
+-----------------------------------------------------+
性能优势:
- 非阻塞主线程:合成与光栅化在独立线程执行,主线程可继续处理JS或布局。
- 跳过重绘:若仅图层位置变化(如CSS
transform
),无需重新绘制内容。
总结:
阶段 | 合成(Composite) |
---|---|
核心目标 | 高效合并图层,输出最终帧 |
性能影响 | 合成复杂度影响帧率 |
优化手段 | 减少图层尺寸、优先光栅化视口内容 |
触发条件 | 图层位置/透明度变化、图块更新 |
光栅化完成后,浏览器会将生成的位图数据交给合成线程和 GPU,完成以下步骤:
1. 图块的合成
- 图块(Tiles):
- 光栅化阶段将页面的每个图层分割成多个图块(通常是 256x256 像素)。
- 每个图块都包含光栅化后的位图数据。
- 合成线程的工作:
- 合成线程负责将这些图块组合起来,形成完整的页面图像。
- 合成线程会根据页面的滚动、动画或用户交互,动态调整图块的排列和显示顺序。
2. GPU 的参与
-
GPU 加速:
- 合成线程将图块的位图数据传递给 GPU。
- GPU 使用其强大的并行计算能力,将多个图块快速合成到一个帧缓冲区(Frame Buffer)中。
-
图层的合成:
- 如果页面包含多个图层(Layer),GPU 会按照图层的顺序(如 z-index)将它们叠加起来。
- GPU 还会处理透明度、混合模式、遮罩等效果。
3. 帧的生成
-
生成帧:
- GPU 将合成后的图像存储在帧缓冲区中,形成一帧完整的页面图像。
- 每一帧对应屏幕上的一个静态画面。
-
帧率:
- 浏览器通常以 60FPS(每秒 60 帧)的速率生成帧,以确保页面的流畅性。
- 如果页面内容复杂或主线程被阻塞,帧率可能会下降,导致页面卡顿。
4. 显示到屏幕
- 显示器刷新:
显示器会按照固定的刷新率(通常是 60Hz)从帧缓冲区中读取图像数据,并将其显示到屏幕上。
这一步由操作系统和显示驱动程序完成。 - 垂直同步(VSync):
- 为了避免屏幕撕裂(Tearing),浏览器会与显示器的刷新率同步,确保每次显示完整的一帧图像。
现代浏览器通过以下优化技术,提升合成和显示的性能:
1. 独立图层
-
独立图层的优势:
- 频繁变化的元素(如动画、滚动)被提升为独立图层,避免影响其他图层。
- 这些图层可以单独光栅化和合成,减少不必要的重绘。
-
触发独立图层的条件:
- 使用 CSS 属性 transform、opacity、will-change。
- 使用 position: fixed 或 position: sticky。
2. GPU 加速
-
GPU 的作用:
- GPU 专门用于处理图形计算,能够快速完成光栅化和合成操作。
- 通过 GPU 加速,浏览器可以显著提升动画和滚动的流畅性。
3. 按需绘制
- 可见区域优先:
- 合成线程只处理当前屏幕可见区域的图块,延迟处理不可见区域。
- 这减少了不必要的计算和内存占用。
分层与合成的性能优化实践:
1. 动画优化
-
优先使用
transform
和opacity
: 这两个属性仅触发合成阶段,跳过布局与绘制。/* 高效动画 */ .box {transition: transform 0.3s; } .box:hover {transform: translateX(100px); }
-
避免
top/left
动画:- 修改
top/left
会触发布局与重绘,性能较差。
- 修改
2. 图层管理
-
按需提升图层:
- 使用
will-change
或transform3d
提升需频繁变化的元素。.animated-element {will-change: transform; /* 提前告知浏览器优化 */ }
- 使用
-
避免过度分层:
- 通过Chrome DevTools的 Layers 面板监控图层数量,合并冗余图层。
3. 减少合成层尺寸
- 裁剪不可见内容:
- 对离屏(Offscreen)元素使用
clip-path
或overflow: hidden
,减少光栅化区域。
- 对离屏(Offscreen)元素使用
拓展
处理 JavaScript 和 CSS
在页面渲染过程中,JavaScript 和 CSS 的处理方式有所不同,它们会影响渲染流程:
CSS 处理
- 样式阻塞渲染:
- CSS 是渲染阻塞资源,它不会直接阻塞 HTML 文档的解析。CSS 是渲染阻塞资源,这意味着浏览器会暂停渲染(构建渲染树)直到 CSS 资源加载和解析完成。这是因为 CSS 直接影响页面的布局和样式,浏览器需要知道元素的样式才能正确地进行布局和绘制。
- 内嵌 CSS:
- 内嵌 CSS(在
<style>
标签中)会在 HTML 文档解析过程中直接解析和应用,并且会直接参与到渲染树的构建中,不会引发额外的网络请求。
- 内嵌 CSS(在
- 外链 CSS:
- 外链 CSS(通过
<link>
标签引入)会引发额外的网络请求。浏览器会暂停构建渲染树,直到 CSS 文件下载并解析完成。
- 外链 CSS(通过
JavaScript 处理
- 脚本阻塞解析:
- 默认情况下,JavaScript 是解析阻塞的。当浏览器遇到
<script>
标签时,会暂停解析 HTML 文档,直到脚本下载、解析和执行完成。这是因为脚本可能会动态修改 DOM 或 CSSOM。
- 默认情况下,JavaScript 是解析阻塞的。当浏览器遇到
- 内嵌 JavaScript:
- 内嵌 JavaScript(在
<script>
标签中)会在 HTML 文档解析过程中直接解析和执行,不会引发额外的网络请求。
- 内嵌 JavaScript(在
- 外链 JavaScript:
- 外链 JavaScript(通过
<script src="...">
引入)会引发额外的网络请求。浏览器会暂停解析 HTML 文档,直到脚本下载、解析和执行完成。
- 外链 JavaScript(通过
- 异步和 defer 属性:
- 使用
async
属性可以使脚本异步加载和执行,不会阻塞 HTML 文档的解析。 - 使用
defer
属性可以使脚本在文档解析完成后但页面加载完成前执行,也不会阻塞 HTML 文档的解析。
- 使用
内嵌和外链资源的加载方式
内嵌资源
- 内嵌 CSS:
<style>body {font-family: Arial, sans-serif;} </style>
- 直接在 HTML 文档中定义,不会引发额外的网络请求。
- 在 HTML 文档解析过程中直接解析和应用。
- 内嵌 JavaScript:
<script>console.log("Hello, World!"); </script>
- 直接在 HTML 文档中定义,不会引发额外的网络请求。
- 在 HTML 文档解析过程中直接解析和执行。
外链资源
- 外链 CSS:
<link rel="stylesheet" href="styles.css" />
- 引发额外的网络请求来获取 CSS 文件。
- 浏览器会暂停构建渲染树,直到 CSS 文件下载并解析完成。
- 外链 JavaScript:
<script src="script.js"></script>
- 引发额外的网络请求来获取 JavaScript 文件。
- 默认情况下,浏览器会暂停解析 HTML 文档,直到脚本下载、解析和执行完成。
- 使用
async
或defer
属性可以改变这种行为。
总结
- 内嵌资源:直接在 HTML 文档中定义,不会引发额外的网络请求,但在 HTML 文档解析过程中处理。
- 外链资源:引发额外的网络请求,CSS 是渲染阻塞资源,JavaScript 是解析阻塞资源,除非使用
async
或defer
属性。
通过合理使用内嵌和外链资源,以及优化资源加载策略(如使用async
和defer
),可以显著提高页面的加载性能和用户体验。
js 的 async 或 defer 属性
在 HTML 中,<script>
标签的 async
和 defer
属性可以控制 JavaScript 脚本的加载和执行方式,它们对于优化页面加载性能非常重要。下面详细介绍这两个属性的区别和用法:
1. async
属性
- 定义 :
async
属性表示脚本应该异步下载和执行。当使用async
时,脚本的下载不会阻塞 HTML 文档的解析,一旦脚本下载完成,它会在 HTML 文档解析过程中被立即执行。 - 执行时机 :脚本会在下载完成后立即执行,具体执行时间不确定,可能在 HTML 文档解析完成之前,也可能在解析完成之后。
async
脚本的执行顺序与它们在 HTML 文档中出现的顺序无关。 - 适用场景 :适用于独立的脚本(如广告代码、分析代码等),这些脚本的执行不依赖于其他脚本,也不会被其他脚本依赖。
2. defer
属性
- 定义 :
defer
属性表示脚本应该在 HTML 文档解析完成后、页面的DOMContentLoaded
事件触发前执行。脚本的下载不会阻塞 HTML 文档的解析,但脚本的执行会被推迟到 HTML 文档解析完成后。 - 执行时机 :脚本的执行顺序与它们在 HTML 文档中出现的顺序一致,确保脚本按顺序执行。
- 适用场景 :适用于需要在 HTML 文档解析完成后执行的脚本,尤其是那些脚本之间有依赖关系的情况。
3. 没有 async
或 defer
属性
- 定义 :默认情况下,脚本会阻塞 HTML 文档的解析,浏览器会暂停解析 HTML 文档,直到脚本下载、解析和执行完成。
- 执行时机 :脚本在下载、解析和执行过程中会阻塞 HTML 文档的解析,导致后续内容无法继续加载。
示例代码
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Script async vs defer</title><script src="script1.js"></script><!-- 默认,阻塞解析 --><script src="script2.js" async></script><!-- 异步,不阻塞解析 --><script src="script3.js" defer></script><!-- 推迟执行,不阻塞解析 --></head><body><h1>Hello, World!</h1></body>
</html>
4. 执行顺序
假设有以下脚本:
<script src="script1.js"></script>
<script src="script2.js" async></script>
<script src="script3.js" defer></script>
<script src="script4.js" async></script>
<script src="script5.js" defer></script>
执行顺序如下:
script1.js
:立即下载并执行,阻塞 HTML 文档解析。script2.js
和script4.js
:异步下载并在下载完成后立即执行,顺序不确定。script3.js
和script5.js
:按顺序下载,但在 HTML 文档解析完成后、DOMContentLoaded
事件触发前按顺序执行。
总结
async
:脚本异步下载和执行,不阻塞解析,执行顺序不确定。defer
:脚本推迟执行,不阻塞解析,执行顺序与 HTML 中的顺序一致。- 默认:脚本阻塞解析,下载、解析和执行完成后继续解析 HTML。
参考:
浏览器的工作方式
浏览器渲染机制
渲染页面:浏览器的工作原理
解析
关键渲染路径
构建对象模型
如何解释CSS新手的"级联"?
浏览器知识点整理(七)渲染流程
相关文章:
HTML页面渲染过程
前言 文章很长,凡是我觉得好的东西统统都塞进来了。看了很多的文章,有些说法甚至都不统一,所以还动用了AI搜索。总之希望这篇文章能有点用,如有错误,欢迎指正。 浏览器介绍 浏览器的主要组件包括: 界面…...
【八股战神篇】Java虚拟机(JVM)高频面试题
目录 专栏简介 一 请解释Java虚拟机(JVM)及其主要功能 延伸 1. JVM的基本概念 2. JVM的主要功能 二 对象创建的过程了解吗 延伸 1.Java 创建对象的四种常见方式 三 什么是双亲委派模型 延伸 1.双亲委派机制的作用: 2.双亲委派模型…...
微店商品详情接口开发指南
接口概述 微店商品详情接口(/api/v1/product/detail)用于获取商品的完整信息,包括标题、价格、库存、SKU、主图等数据,支持OAuth2.0鉴权。 点击获取key和secret 请求方式 GET https://open.weidian.com/api/v1/product/detail …...
拦截指定注解(FeignClient),补偿重试
拦截指定注解(FeignClient),补偿重试;对代码无入侵 避免正常调用和重试逻辑调用重复插入; 根据自己的业务需求 插入新数据时 是否需要删除之前的旧数据,防止数据覆盖 import cn.hutool.core.util.ObjectUti…...
使用 GitHub Pages 部署单页面应用教程
## 简介 GitHub Pages 是 GitHub 提供的一个静态网站托管服务,可以免费托管个人、项目或组织页面。本教程将指导您如何部署一个单页面应用到 GitHub Pages。 ## 前提条件 - 拥有 GitHub 账号 - 已安装 Git - 已安装 Node.js(如果使用前端框架&#x…...
day16-17-磁盘管理
1. 磁盘分类 磁盘接口 硬盘 大小 sata接口 机械硬盘、固态硬盘 机械:4tb 10k性能要求不高 sas接口 机械硬盘、固态硬盘 机械:900G 15k性能好,容量低 pcie-e接口 固态硬盘 tb级别 4tb 8tb 性能要求高,数据库,…...
【神经网络与深度学习】扩散模型之通俗易懂的解释
引言: 扩散模型(Diffusion Models)是近年来深度学习领域的一项重要突破,尤其在生成式人工智能(Generative AI)中展现了惊人的能力。它的核心思想类似于一个孩子学习搭建乐高城堡的过程——先拆散࿰…...
Linux Bash 中 $? 的详细用法
Bash (Bourne Again SHell) 是使用最广泛的 SHell 脚本语言之一,因为它与 Unix 和 Linux 系统兼容。它提供了许多内置函数和变量,使脚本编写更高效,更不容易出错。其中一个变量是 $?, 它是 Bash 脚本错误处理的一个组成部分。这个…...
嵌入式培训之系统编程(一)标准IO、文件操作
目录 一、系统编程概述 二、标准IO (一)(以计算机为中心)标准IO (二)io的分类 (三)man命令 三、文件读写操作 (一)文件操作步骤 (二&#…...
NVIDIA Earth-2 AI 天气模型 DLI 课程:解锁全球风云的未来之匙
电影闲聊引发思索之言: 曾几何时,当我们闲聊起那些描绘美国气候的大电影时(龙卷风-后天等美国大片),仿佛被带入了一个个奇幻而真实的气象世界。从狂风暴雨到烈日炎炎最后到冰天雪地,电影里的场景让我们对气…...
至此(day1-day4)代码详解(ai辅助整理)
至此(day1-day4)代码详解 ipl10.nas ; 第一阶段引导程序 ; 功能:读取磁盘数据并跳转到第二阶段加载程序 ; 编译参数:nask -o ipl10.bin ipl10.nasCYLS EQU 10 ; 预设读取柱面数(实际值由BIOS决定)ORG…...
STM32F103_LL库+寄存器学习笔记12.2 - 串口DMA高效收发实战2:进一步提高串口接收的效率
导言 通过优化代码算法,在串口空闲中断回调里不需要暂时关闭DMA接收,达到提高串口接收的效率。在IDLE接收中断里关闭DMA接收会导致接收过程中有数据丢失风险(关DMA的瞬间如果有数据到来,会丢帧!)。 回顾一…...
conda 设置env后,环境还是安装在c盘的解决方式:
1|设置 envs 文件夹权限 右键【envs】文件夹,选择【属性】 选择【安全】,点击【编辑】 选中【Users(用户名\Users)】,选中运行所有权限,如图所示 点击【确认】,确保修改被保存 2、环境变量path设置 选择【高级系统设置…...
设计模式 - 工厂模式
简单工厂模式 public class CoffeeFactory {public Coffee get(string coffeeType) {Coffee coffee null;if ("American".equals(coffeeType)) {coffee new AmericanCoffee();} else if ("Latte".equals(coffeeType)) {coffee new LatteCoffee();}retur…...
动态规划-LCR 090.打家劫舍II-力扣(LeetCode)
一、题目解析 本题与打家劫舍的最大区别在于房子不是线性分布的了,而是首尾相连的环形分布,即如果偷了第一间房子,那么最后一间房子就不能偷了,因为它们是相连的。 二、算法原理 在分析之前我们可以先讨论上面提到的第一间房子偷…...
2025 年暑假 LBE 大空间市场火爆程度预测:技术驱动与消费升级下的增长引擎
一、市场爆发的底层逻辑 根据 DeepSeek 行业报告显示,2025 年 LBE 大空间市场将呈现结构性爆发,核心驱动力来自三大技术突破: 空间计算能力跃迁:上海移动已开通全球最大规模商用 5G-A 3CC 网络,主城区及十大重点场景…...
【AI 大模型】盘古大模型简介 ( 创建空间 | 体验模型 | 部署模型 )
文章目录 一、盘古大模型简介1、创建空间2、体验模型3、部署模型 总结 : 盘古大模型 是 开发部署 盘古基础模型 , 或 在 盘古模型 基础上进行 微调训练 的 大模型 的平台 , 是 开发训练 大模型的平台 ; 不适合 中小企业 和 个人开发者 开发 大模型应用 ; 一、盘古大模型简介 1、…...
2025年护网行动蓝队防御全解析:构建智能动态防御体系
2025年,随着网络攻击手段的智能化、混合化升级,护网行动中的蓝队防御已从传统的被动防护转向“动态感知、智能研判、主动反制”的立体化模式。如何在攻防不对称的对抗中实现“看得见、防得住、溯得清”?本文将结合前沿技术与实战经验…...
【Java高阶面经:微服务篇】3.熔断机制深度优化:从抖动治理到微服务高可用架构实战
一、熔断抖动的本质剖析与核心成因 1.1 熔断机制的核心价值与抖动危害 熔断机制作为微服务弹性架构的核心组件,通过模拟电路断路器逻辑,在服务出现异常时自动阻断请求链,防止故障扩散引发雪崩。但频繁的“熔断-恢复-熔断”抖动会导致: 用户体验恶化:请求成功率波动大,响…...
HTML回顾
html全称:HyperText Markup Language(超文本标记语言) 注重标签语义,而不是默认效果 规则 块级元素包括: marquee、div等 行内元素包括: span、input等 规则1:块级元素中能写:行内元素、块级元素(几乎什么都能写) 规则2:行级元素中能写:行内元素,但不能写:块…...
Leetcode百题斩-字典树
208. Implement Trie (Prefix Tree)[medium] 做完了哈希,来看看数据结构,做做字典树。字典树在搜索方面的作用还是蛮大的,主要是能实现前缀联想以及正确性匹配相关的功能。 字典树又名前缀树,顾名思义就是维护字符串的前缀。这个…...
大数据Spark(五十九):Standalone集群部署
文章目录 Standalone集群部署 一、节点划分 二、搭建Standalone集群 1、将下载好的Spark安装包上传解压 2、配饰spark-env.sh 3、配置workers 4、将配置好的安装包发送到node2、node3节点上 5、启动Standalone集群 三、提交任务测试 Standalone集群部署 Standalone 模…...
Vue 3 ~ 3.5 版本useTemplateRef使用
注意,useTemplateRef版本要在 3.5 以后才可使用,版本低的 ref 替代问题也不大~ 2024 年 9 月 1 日发布的 组合式 API:辅助 | Vue.js,引入一个小小的新 API useTemplateRef(),它用于访问实际的 DOM 节点。 …...
使用F5-tts复刻音色
最近第一人称视角的视频很火,想试试看复刻一下电视剧中某个角色的音色。看了下字节的API,嗯。。。138元一个音色,还不包括合成语音的费用,算了还是看看开源项目吧。 随便搜了搜,发现了两个项目一个是openvoice&#x…...
使用亮数据代理IP+Python爬虫批量爬取招聘信息训练面试类AI智能体(附完整源码)
文章目录 一、为什么要用代理IP?(重要!)二、环境准备(5分钟搞定)三、爬虫核心代码解析(含反反爬技巧)四、数据清洗的3个关键步骤五、训练AI智能体的实战技巧六、法律风险防范(必须看!…...
[软件工程]第二章题目汇总
1 [单选题] 原型化模型是( )。 A、适用于客户需求被明确定义的情况 B、很难产生有意义产品的一种冒险模型 C、提供一个精确表述的形式化规格说明 D、适用于客户需求难以清楚定义的情况 2 [单选题] 下列关于增量模型的说法正确的是( &…...
Java EE进阶1:导读
1.发展历程 2.学习内容 前⾯的课程中,学习的是Java基础,JavaEE主要学习Java的应用,也就是学习Java在企业中是如何应用的 Java更多场景是业务开发,更狭义点可以理解为web开发.所以咱们的学习也是围绕着如何使用Java来做web开发 2.1 什么是Web开发? web(…...
Unity自定义shader打包SpriteAtlas图集问题
Unity打包图集还是有一些坑的,至于图集SpriteAtlas是什么请参考我之前写的文章:【Sprite Atlas】Unity新图集系统SpriteAtlas超详细使用教程_spriteatlas 使用-CSDN博客 问题: 今天碰到的问题是,shader绘制的时候,因…...
系统集成项目管理工程师学习笔记之启动过程组
第十章 启动过程组 制定项目章程 定义 制定项目章程是编写一份正式批准项目并授权项目经理在项目活动中使用组织资源的文件的过程。 正式批准的项目文件 作用 1、明确项目与组织战略目标之间的直接联系 2、确立项目的正式地位 3、展示组织对项目的承诺 本过程仅开展一…...
vscode 常用调试
一、文件执行 python script.py {"name": "Python 调试程序: 当前文件","type": "debugpy","request": "launch","program": "${file}","console": "integratedTerminal"…...
Java 07异常
异常 指的是程序在编译和执行的过程中,出现的非正常的情况; 当然语法错误并不属于错误异常体系 最大的Throwable; 分为两个:Error ExceptionError 严重级别问题 常见的 堆内存溢出 栈内存溢出Exception 分为两个子类 RuntimeException 运…...
2025年PMP 学习二十三 16章 高级项目管理
2025年PMP 学习二十三 16章 高级项目管理 文章目录 2025年PMP 学习二十三 16章 高级项目管理高级项目管理战略管理战略管理的组成要素:企业战略转化为战略行动的阶段: 组织战略类型战略组织类型组织级项目管理OPM(公司项目管理) 组…...
【Java高阶面经:微服务篇】1.微服务架构核心:服务注册与发现之AP vs CP选型全攻略
一、CAP理论在服务注册与发现中的落地实践 1.1 CAP三要素的技术权衡 要素AP模型实现CP模型实现一致性最终一致性(Eureka通过异步复制实现)强一致性(ZooKeeper通过ZAB协议保证)可用性服务节点可独立响应(支持分区存活)分区期间无法保证写操作(需多数节点可用)分区容错性…...
ISCC 2025决赛 wp
PWN Dilemma 64位程序没有开启PIE,并且过滤了execve,不能使用system这些的了,所以要考虑ORW来做 进入main函数分析,这里有两个函数一个func_1一个func_2。 这两个函数都有漏洞,以下是详细分析: 对于func…...
C++(5)switch语句 循环while
这是一个电影评分的程序 default 就是 如果上述的都没有执行 就统一的执行default的内容。 然后记得break ___________________________________ 循环 (while) while的使用方式 输出 0-9的while循环...
操作系统----软考中级软件工程师(自用学习笔记)
目录 1、计算机系统层次结构 2、程序顺序执行的特征 3、程序并发执行的特征 4、三态模型 5、同步与互斥 6、信号量机制 7、PV操作 8、死锁 9、进程资源图 10、死锁避免 11、线程 12、程序局部性原理 13、分页存储管理 14、单缓冲器 15、双缓冲区 16、磁盘调度算…...
利用Spring Boot和Redis构建高性能缓存系统
利用Spring Boot和Redis构建高性能缓存系统 引言 在现代Web应用中,缓存是提升系统性能的关键技术之一。Redis作为一种高性能的内存数据库,广泛应用于缓存场景。本文将介绍如何利用Spring Boot和Redis构建一个高性能的缓存系统,涵盖Redis的基…...
每日一题:1、虚拟IPv4地址转换为32位整数(JS)
题目背景 我们需要处理一种特殊的虚拟IPv4地址,这种地址由4个小节组成,每节之间用#分隔。与标准IPv4地址不同,虚拟IPv4地址的第一节范围是1~128,后三节的范围是0~255。我们需要将这种虚拟IPv4地址转换为一个唯一的32位整数。如果…...
[Vue]组件介绍和父子组件间传值
组件介绍 Vue3的 .vue文件中的主要部分分别分为三个:<template>、<script>、<style> <template>: 结构,相当于原html中的<head><body><footer>部分。原本的index.html现在只做一个容器࿰…...
Vue3 中使用 provide/inject 实现跨层级组件传值失败的原因及解决方案
1、基础用法 父组件: <script setup> import { ref, provide } from vue; import ChildComponent from ./ChildComponent.vue; const parentData ref(初始数据); // 提供数据 provide(parentData, parentData); </script>子组件: <sc…...
Git Hooks 和 自动生成 Commit Message
前言: 企业编程必须始终依赖流程,而不是个人。个人能力很重要,应该鼓励,但不能指望它,否则软件质量将不一致,没有可持续性。一旦顶级程序员跳槽,公司就会陷入困境。企业应该努力改进工作流程&am…...
【小明剑魔视频Viggle AI模仿的核心算法组成】
Viggle AI 作为一款先进的生成式视频AI工具,其核心技术栈融合了多项前沿算法。以下是深度解析其核心算法架构及实现原理: 一、核心算法组成 1. 运动控制生成(Motion Control Generation) 算法框架:基于扩散模型&…...
Linux学习心得问题整理(二)
day05 Linux基础入门 Linux语法解析 如何理解ssh远程连接?如何使用ssh使用远程连接服务? ssh进也称远程服务终端,常见连接方式可以包括windows和Linux两种方式 首先咱们使用windows窗口进行连接,这里就采用xshell连接工具来给大家做演示吧…...
百度网盘加速补丁v7.14.1.6使用指南|PC不限速下载实操教程
软件介绍 本加速补丁可突破百度网盘限速限制,无需会员、无次数限制,实测下载速度可达带宽峰值。 三步极速配置教程 1. 环境准备 → 卸载电脑原有百度网盘客户端(避免冲突) → 关闭杀毒软件/安全卫士(防止误删补丁&am…...
RocketMQ消息拉取模式详解
RocketMQ提供了两种消息拉取模式,Pull模式(主动拉取)和 Push模式(长轮询)。 一、消息拉取模式分类 1. Pull模式(主动拉取) 特点:消费者主动向Broker发送请求拉取消息实现类&#…...
C++23 容器从其他兼容范围的可构造性与可赋值性 (P1206R7)
文章目录 背景与动机提案内容与实现细节提案 P1206R7实现细节编译器支持 对开发者的影响提高灵活性简化代码向后兼容性 总结 C23标准引入了对容器构造和赋值的新特性,这些特性使得容器能够更灵活地从其他兼容范围初始化,并支持从范围赋值。这些改进由提案…...
深入解析 HTTP 中的 GET 请求与 POST 请求
在互联网的世界里,数据的传输与交互无时无刻不在发生。HTTP(超文本传输协议)作为 Web 应用的基石,承载着浏览器与服务器之间的通信重任。而 GET 请求和 POST 请求,作为 HTTP 协议中最为常用的两种请求方法,…...
华三(H3C)IRF堆叠心跳的LACP MAD、BFD MAD和ARP MAD差异
华三(H3C)IRF堆叠心跳的三种MAD(多主检测)机制——LACP MAD、BFD MAD和ARP MAD在实现原理、组网要求及适用场景上存在显著差异。以下是三者的对比分析: 一、核心区别对比 特性LACP MADBFD MADARP MAD检测原理扩展LAC…...
thread 的mutex优化
std::mutex mtx; int shared_data 0;void increment() {std::lock_guard<std::mutex> lock(mtx); // 自动加锁shared_data; // 临界区 } // 离开作用域时自动解锁std::lock_guard 在离开作用域时自动解锁的行为是基于 C 的 RAII (Resource Acquisition Is Initializa…...
深入解析前端 JSBridge:现代混合开发的通信基石与架构艺术
引言:被低估的通信革命 在移动互联网爆发式增长的十年间,Hybrid App(混合应用)始终占据着不可替代的地位。作为连接 Web 与 Native 的神经中枢,JSBridge 的设计质量直接决定了应用的性能上限与开发效率。本文将突破传…...