前情提要:本文章适用于那些初学者学习前端,为了不影响后面的知识学习,想要快速且完整了解浏览器执行原理的同学🚀,全篇读完大约十分钟😄
如果想要深入了解,可以看我这篇文章,里面对此进行了详细说明🏃♀️
网页的解析过程
大家有没有深入思考过:一个网页URL从输入到浏览器中,到显示经历过怎么样的解析过程呢?🤔 我们这里简单概述一下
-
URL 解析:将输入的 URL 分解为协议、域名、路径等部分。
-
DNS 解析:将域名解析为服务器的 IP 地址。
-
建立连接:
- 使用 TCP 完成三次握手。
- 若为 HTTPS,还需进行 TLS 加密握手。
4.发送 HTTP 请求:浏览器向服务器发送请求(如 GET)。
5.服务器响应:服务器处理请求并返回 HTML、CSS、JS 等资源。
6.内容解析与渲染:
diff
- 浏览器解析 HTML,构建 DOM 树。
- 解析 CSS,生成样式表。
- 执行 JavaScript,动态修改 DOM。
- 进行布局计算和绘制。
7.资源加载:并行请求其他资源(如图片、视频、额外脚本)。
8.页面显示与交互:渲染完成后用户可操作,浏览器持续处理更新。
接下来让我们看看下面这张图,可以大概了解浏览器在构建我们页面的流程
可以总结出来浏览器在构建我们页面的大致流程为
- 输入服务器地址:用户在浏览器中输入 URL,浏览器向服务器发起请求。
- 返回 HTML 文件 :服务器响应请求,将主页面文件(例如
index.html
)返回给浏览器。 - 解析 HTML 文件
- 浏览器开始解析 HTML 文档,当遇到特定的资源引用标签时,会发起额外请求:
<link>
元素:遇到link元素,就回去解析link元素里面的内容<script>
元素:遇到script标签就会解析js文件
- 加载 CSS 文件 : 浏览器解析
link
标签后,向服务器请求 CSS 文件(如xxx.css
),以加载页面样式。 - 加载 JavaScript 文件 :浏览器解析
script
标签后,向服务器请求 JavaScript 文件(如xxx.js
),用于动态行为。 - 资源下载完成:CSS 和 JavaScript 文件分别返回给浏览器,并与 HTML 结合,最终渲染完整页面。
注意⚠️:在我们部署静态资源的时候,一般情况在下载下来的都是html,文件,如果碰到link元素,再去解析css,碰到script元素再去下载js文件
浏览器内核
想对浏览器内核进行了解的,可以看这篇文章➡️
渲染引擎如何解析页面呢?
渲染引擎在拿到一个页面后,如何解析整个页面并且最终呈现出我们的网页呢?🤔
我们对下面这张图进行分析,这张图请你刻在脑子里面😡
1. HTML 文件的解析
-
输入:HTML 文件。
-
过程 :通过 HTML Parser (HTML 解析器)解析 HTML,生成 DOM 树(Document Object Model)。
- DOM 树是一种树状结构,表示 HTML 文档的逻辑结构。
2. CSS 文件的解析
-
输入:CSS 样式表。
-
过程 :通过 CSS Parser (CSS 解析器)解析 CSS,生成 Style Rules(样式规则)。
- 这些样式规则包含了页面元素的外观定义(颜色、大小、布局等)。
3. DOM 树与样式的结合
- 过程 :将解析得到的 DOM 树 和 Style Rules 进行关联,形成 Attachment(附加)。
- 结果:在 DOM 树的基础上,将样式应用到每个对应的节点。
4. 生成 Render Tree(渲染树)
-
过程 :根据 DOM 树和样式规则,生成 Render Tree。
- 渲染树是为显示页面内容而创建的树结构,包含页面可见元素及其样式信息。
- 不可见元素(如
display: none
)不会出现在渲染树中。
5. 布局(Layout)
-
输入:渲染树。
-
过程:根据渲染树计算每个元素的位置和大小。
- 布局阶段负责确定页面上每个元素的几何信息(即页面布局)。
6. 绘制(Painting)
- 输入:经过布局计算的渲染树。
- 过程:将每个元素的样式和几何信息转换为实际像素,绘制到屏幕的绘图缓冲区。
7. 显示(Display)
- 过程:浏览器将绘制好的内容展示在屏幕上,完成整个渲染流程。
接下来我们进行详细分析每个阶段
解析一:解析html
因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始:
解析HTML,会构建DOM Tree:
解析二:解析css
-
在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件:
-
注意:下载CSS文件是不会影响DOM的解析的;
-
浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:
-
我们可以称之为 CSSOM(CSS Object Model,CSS对象模型);
注意:解析html和css都是有自己的线程,是不会相互阻塞的,不会影响DOMtree生产过程,
为什么呢?🤔
1. DOM 和 CSSOM 是独立的解析过程
-
DOM 的解析 :浏览器会从上到下解析 HTML 文件,构建 DOM 树。这是一个流式操作,边解析边构建 DOM。
-
CSSOM 的解析 :当遇到
<link>
标签时,浏览器会异步请求对应的 CSS 文件,并在收到后开始解析它,生成 CSSOM。 -
这两个过程是独立的:
- 浏览器可以继续解析 HTML 文档,构建 DOM 树。
- 不需要等待 CSS 文件加载完成或解析完成才继续 DOM 的解析。
2. 解析过程是并行的
- 浏览器具有并行能力 ,允许 HTML 的解析 和 CSS 的下载 同时进行。
- 在遇到
<link>
标签时,浏览器会将 CSS 文件的下载任务交给网络线程 ,而 DOM 的解析继续在主线程中执行。 - 这种机制确保了解析 HTML 的效率,不会因为外部资源加载而阻塞
还有一点注意⚠️,虽然他们两个不会互相影响,但是他们一般都会影响RenderTree的生成(但是也不一定,具体看浏览器的优化)
3. 关键路径优化(Critical Rendering Path Optimization)
-
关键渲染路径:包括构建 DOM 树、CSSOM 和渲染树的核心部分。
-
浏览器会优先下载和解析被标记为 "关键资源" 的 CSS 文件。
- 如果 CSS 文件体积较小且加载较快,浏览器可以优先完成其 CSSOM 的生成。
- 非关键的 CSS(如
media
查询不匹配的样式)可能被延迟加载或解析。
4. 部分渲染
-
对于未受样式影响的 DOM 节点,浏览器可能会 提前构建部分 Render Tree 并渲染。
- 例如,对于 HTML 文件中靠前的元素,可能先渲染无样式版本,然后在 CSSOM 构建完成后再应用样式重新渲染。
- 这种方式可以减少页面的白屏时间。
5. 渐进渲染(Progressive Rendering)
-
浏览器可能分段处理 DOM 树和 CSSOM,例如:
- 构建初步的 Render Tree,渲染内容。
- 待后续 CSS 文件完成加载后,再更新渲染树并重新渲染。
-
这种优化可以显著提升用户的感知性能。
6. 异步或非阻塞加载
- 非阻塞的 CSS 文件加载 :通过
<link>
标签的media
属性或动态加载的 CSS 文件,浏览器可能会延迟加载某些非关键的样式,优先渲染重要内容
构建Render Tree
当有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建Render Tree了
- 注意一:link元素不会阻塞DOM Tree的构建过程,但是会阻塞Render Tree的构建过程
- 注意二:Render Tree和DOM Tree并不是一一对应的关系,比如对于display为none的元素,压根不会出现在render tree中;
注意 1.默认情况下Rendertree上的节点是没有每个节点大小和位置信息,只有通过layout解析后,才会被解析出来。
- DOM Tree和Render Tree不一定是一一对应的。
这是为什么呢🤔?
1.默认情况下Rendertree上的节点是没有每个节点大小和位置信息 ,只有通过layout 解析后,才会被解析出来。
为什么呢?🤔
Render Tree 的初步生成 仅仅是将 DOM 节点 和 样式规则 (Style Rules)结合,表示哪些元素需要被绘制以及它们的基本样式信息。- 元素的具体几何信息(大小和位置)只有在 Layout 阶段 才会通过计算得出。这是因为,布局阶段会综合考虑多种因素,如:
- CSS 布局模型(如
box model
)。 - 元素的父子关系和依赖(例如,子元素的大小依赖于父元素的大小)。
- 浏览器窗口的尺寸(特别是响应式布局)。
因此,只有经过 Layout 阶段 的几何计算后,Render Tree 上的节点才会获得大小和位置信息。在这之前,渲染树中的节点仅表示页面的结构和样式逻辑。
- DOM Tree和Render Tree不一定是一一对应的
这是为什么呢?🤔
1. 不可见的元素不进入 Render Tree
某些 DOM 节点虽然存在于文档中,但由于样式规则或者 HTML 属性的影响,并不会被绘制。例如:
-
元素被设置为
display: none
:- 这类元素完全从 Render Tree 中排除,因为它们既不可见,也不占据空间。
-
注释节点、
<script>
和<meta>
等非渲染内容的节点:- 这些 DOM 节点不会参与页面渲染,因此不再 Render Tree 中出现。
2. 伪元素的引入
- Render Tree 中可能包含 CSS 伪元素 (如
::before
和::after
),这些伪元素并不存在于 DOM Tree 中,但它们会被渲染引擎加入 Render Tree,表示需要绘制的内容。
3. 某些节点被拆分为多个渲染节点
-
一个 DOM 节点可能对应多个 Render Tree 节点。例如:
- 如果一个元素有多种样式分段(如文本内容中有部分文字用
span
包裹并应用了不同的样式),它可能在 Render Tree 中被分解成多个节点。 - 例如,带有复杂 CSS 样式的
<p>
元素可能在 Render Tree 中被拆分为多个部分。
- 如果一个元素有多种样式分段(如文本内容中有部分文字用
4. 不同的渲染模型
- 一些复杂布局(如表格布局)会在 Render Tree 中使用额外的辅助节点来表示,甚至可能重组部分节点关系,这使得 DOM 和 Render Tree 的映射更加复杂。
解析四 -- 布局(layout)和绘制(Paint)
第四步是在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。
-
渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;
-
布局是确定呈现树中所有节点的宽度、高度和位置信息;
-
第五步是将每个节点绘制(Paint)到屏幕上
-
在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点;
-
包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)
如下图所示
回流和重绘
回流
理解回流reflow:(也可以称之为重排)
-
第一次 确定节点的大小和位置,称之为布局(layout)。
-
之后 对节点的大小、位置修改重新计算 称之为回流。
这里我们看代码,将它运行到浏览器
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="box">box盒子</div>
<button>修改box</button>
<script>
var boxEl = document.querySelector(".box")
var btnEl = document.querySelector("button")
btnEl.onclick = function() {
boxEl.remove()
boxEl.style.height = "200px"
}
</script>
</body>
</html>
css
body {
padding: 0;
margin: 0;
}
.box {
width: 100px;
height: 100px;
background-color: orange;
font-size: 15px;
color: red;
}
当我们点击按钮的时候发现盒子的大小改变了,但是按钮的位置也变化了,也就是整个布局发生变化。这种情况就是回流
什么情况下引起回流呢?
比如DOM结构发生改变:(添加新的节点appendChild()
或 或者移除节点removeChild()
);
动态修改 DOM 结构 :例如,通过 innerHTML
或 document.createElement()
插入新的 HTML 元素。
js
const element = document.createElement('div');
document.body.appendChild(element); // 添加元素,触发回流
- 比如改变了布局(修改了width、height、padding、font-size,top,position等值)
js
const element = document.getElementById('box');
element.style.width = '200px'; // 改变宽度,触发回流
改变页面整体布局(修改了窗口的尺寸等)
浏览器窗口大小变化:用户调整窗口大小会触发回流,因为需要重新计算布局。
滚动操作:滚动条的移动可能触发页面部分元素的重新布局
获取需要布局计算的属性
- 比如调用getComputedStyle方法获取尺寸、位置信息;(注意,这个不一定,如果只是回去信息,不进行额外操作有些浏览器不会引起回流,具体看浏览器的优化)
改变内容(文字或图片) 动态修改文字内容或加载新图片会影响元素的尺寸,从而触发回流。 动画和过渡效果
-
使用 CSS 动画或过渡(
transition
)可能涉及布局属性的变化,比如width
、height
、top
、left
。 -
如果动画过程中频繁更改布局相关属性,会导致多次回流。
表格相关的操作 表格的布局(如<table>
元素)比普通的块级元素更复杂,任何涉及表格内容或结构的变化通常都会引起回流。
重绘repaint:
是指浏览器重新绘制页面元素的外观(如颜色、边框、背景等)的过程,但不涉及布局的重新计算(即元素的大小和位置未改变)
-
第一次渲染内容称之为绘制(paint)。
-
之后重新渲染称之为重绘。
什么情况下会引起重绘呢?
比如修改背景色、文字颜色、边框颜色、样式,阴影,透明度等,总而言就是样式属性,而这些属性不会影响元素的几何形状或文档的布局;
总结
回流一定会引起重绘,所以回流是一件很消耗性能的事情, 所以在开发中要尽量避免发生回流:
如何避免回流?
-
1.修改样式时尽量一次性修改 比如通过cssText修改,比如通过添加class修改
-
2.尽量避免频繁的操作DOM,我们可以在一个DocumentFragment或者父元素中
-
将要操作的DOM操作完成,再一次性的操作;
-
3.尽量避免通过getComputedStyle获取尺寸、位置等信息;
-
4.对某些元素使用position的absolute或者fixed,但这种方法并不是不会引起回流 ,而是将这些元素从 文档流 中移除后,只会影响它自身的布局 ,而不会触发整个页面的重新布局不会对其他元素造成影响,开销较小。 这就是后面说到的composite了。
总结
特殊解析-composite合成
绘制的过程,可以将布局后的元素绘制到多个合成图层中,这是浏览器的一种优化手段。
- 默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的;
标准流就是一个网页内的标签按照规定好的默认方式,从上到下、从左到右顺序排列。
块级元素会独占一行,从上到下顺序排列
常用元素:div、hr、p、h1~h6、ul、ol、dl、form、table
行内元素会按照顺序,从左到右顺序排列,碰到父元素自动换行
常用元素:span、a、i、em等。
- 而一些特殊的属性,会创建一个新的合成层( CompositingLayer ),并且新的图层可以利用GPU来加速绘制, 因为每个合成层都是单独渲染的;
那么哪些属性可以形成新的合成层呢?
常见的一些属性:
-
3D transforms
-
video、canvas、iframe
-
opacity 动画转换时;
-
position: fixed
-
will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;
-
animation 或 transition 设置了opacity、transform;
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box, .container {
width: 200px;
height: 200px;
background-color: orange;
/* will-change: transform; */
}
.container {
background-color: red;
/* 会生成新的图层 */
/* position: fixed; */
/* transform: translateZ(0); */
/* opacity: 0.9;
transition: all 1s ease; */
will-change: transform;
}
/* .container:hover { */
/* transform: translateX(100px); */
/* opacity: 0.2; */
/* margin-left: 100px; */
/* } */
</style>
</head>
<body>
<div class="box"></div>
<div class="container"></div>
</body>
</html>
结果 省下的属性你们可以自己解除注释,自己玩玩😄
总结
分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用
script元素和页面解析的关系
我们现在已经知道了页面的渲染过程,但是JavaScript在哪里呢?思考
- 事实上,浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的;
- 它会停止 继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本;
- 只有等到JavaScript脚本执行结束 后,才会继续解析HTML,构建DOM树;
为什么要这样做呢?🤔
- 这是因为JavaScript的作用之一就是操作DOM,并且可以修改DOM;
- 如果我们等到DOM树构建完成并且渲染再执行JavaScript,会造成严重的回流和重绘,影响页面的性能;
- 所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树;
但是这个也往往会带来新的问题,特别是现代页面开发中:
- 在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更"重",处理时间需要更长;
- 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到;
为了解决这个问题,script元素给我们提供了两个属性(attribute):defer和async。
defer和async
defer
- defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree。
- 脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程;
- 如果脚本提前下载好了,它会等待DOM Tree构建完成 ,在DOMContentLoaded事件之前先执行defer中的代码, 所以DOMContentLoaded总是会等待defer中的代码先执行完成。
- 另外多个带defer的脚本是可以保持正确的顺序执行的。
- 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中;
注意:defer仅适用于外部脚本,对于script默认内容会被忽略 即这种形式,为外部脚本
js
<script src="script.js"></script>
对于内联脚本(直接在<script>
标签内写代码)
js
<script>console.log('Hello');</script>
,defer
属性会被忽略。
让我们看下面的代码具体感受下defer
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">app</div>
<div class="box"></div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<--注意力放在这段代码上⚠️⚠️-->
<script src="./js/test.js"></script>
<h1>哈哈哈哈啊</h1>
</body>
</html>
js
console.log("test")
console.log("defer script")
var message = "test message"
//这里打上debugger,阻止继续运行
debugger
结果我们发现程序已经停在debugger的位置,可以发现哈哈哈哈啊 这段文字并没有显示出来,阻止了html解析
好,现在我们给script赋予defer属性
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">app</div>
<div class="box"></div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<script src="./js/test.js" defer></script>
<h1>哈哈哈哈啊</h1>
</body>
</html>
结果发现,即使还是停在了debugger上,但是不影响html的解析
接下来我们看这串代码
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">app</div>
<div class="box"></div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<!-- 1.下载需要很长的事件, 并且执行也需要很长的时间 -->
<!-- 总结一: 加上defer之后, js文件的下载和执行, 不会影响后面的DOM Tree的构建 -->
<script src="./js/test.js" defer></script>
<script>
// 总结三: defer代码是在DOMContentLoaded事件发出之前执行
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})
</script>
<h1>哈哈哈哈啊</h1>
</body>
</html>
js
// console.log("test")
console.log("defer script")
// var message = "test message"
// debugger
// 总结二: 在defer代码中DOM Tree已经构建完成
var boxEl = document.querySelector(".box")
console.log(boxEl)
我们在这段结果中可以总结三点出来:
- 总结一: 加上defer之后, js文件的下载和执行, 不会影响后面的DOM Tree的构建
- 总结二: 在defer代码中DOM Tree已经构建完成
- 总结三: 如果脚本先执行完,那么defer代码是在DOMContentLoaded事件发出之前执行
我们再改进一下代码
js
console.log("test")
console.log("defer script")
var message = "test message"
var boxEl = document.querySelector(".box")
console.log(boxEl)
js
console.log("demo")
console.log(message)
变成
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./js/test.js" defer></script>
<script src="./js/demo.js" defer></script>
</head>
<body>
<div id="app">app</div>
<div class="box"></div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})
</script>
<h1>哈哈哈哈啊</h1>
</body>
</html>
可以总结出来defer解析script是有顺序的
先执行text.js,然后再执行demo.js,不然text message,是无法打印出来的
async
- async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。
- async是让一个脚本完全独立的:
- 浏览器不会因 async 脚本而阻塞(与 defer 类似);
- async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本,很是野蛮啊😡;
- async不会保证在DOMContentLoaded之前或者之后执行;
这里要强调async慎用。他在我眼中是很野蛮的,在能用defer的情况在就用defer,少用async,因为他不能不能保证顺序。 看下面的代码
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./js/test.js" async></script>
<script src="./js/demo.js" async></script>
</head>
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<h1>哈哈哈哈啊</h1>
<div class="box"></div>
</body>
</html>
你可以多刷新几次,有时候会出现这种情况。
建议
- defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;
- async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的;