浏览器底层操作
在浏览器技术中,渲染引擎和JavaScript引擎是两个重要的组成部分,它们在前端和后端开发中扮演着不同的角色。
渲染引擎
浏览器的内核,也被称为渲染引擎,是浏览器的核心组件,负责解析网页的HTML、CSS和JavaScript代码,并将其渲染成用户可以交互的网页。不同的浏览器可能使用不同的内核。
不同的浏览器使用不同的渲染引擎,如:
- WebKit/Blink:用于Safari和Chrome。
- Gecko:用于Firefox。
- Trident:早期的Internet Explorer。
- EdgeHTML:旧版Microsoft Edge。
渲染引擎的工作原理
- 渲染引擎是怎么工作的?
渲染引擎的工作流程可以分为几个关键步骤
-
HTML解析器(DOM Parser)
- 渲染引擎首先解析HTML文档,生成DOM树(Document Object Model Tree)。DOM树是网页结构的树状表示,每个节点代表一个HTML元素。
(html -> DOM树)
- 渲染引擎首先解析HTML文档,生成DOM树(Document Object Model Tree)。DOM树是网页结构的树状表示,每个节点代表一个HTML元素。
-
CSS解析器:
- 然后,渲染引擎解析CSS样式表,生成CSSOM树(CSS Object Model Tree)。CSSOM树是样式规则的树状结构,每个节点包含CSS选择器和相应的样式属性。
-
合并生成渲染树(Render Tree) :
- 渲染引擎将DOM树和CSSOM树结合起来,创建渲染树。渲染树包含了所有需要在屏幕上显示的元素,以及它们的样式信息。(DOM树和CSSOM树合并 -> 渲染树)
-
图层布局计算模块:
- 渲染树生成后,渲染引擎会计算每个节点的精确位置和大小,这个过程称为布局(Layout)或重排(Reflow)。布局树(Layout Tree)是渲染树的一个优化版本,它考虑了元素的可见性、层叠上下文等。( 输入是
render tree渲染树,输出是layout tree布局树(精确的位置和大小))
- 渲染树生成后,渲染引擎会计算每个节点的精确位置和大小,这个过程称为布局(Layout)或重排(Reflow)。布局树(Layout Tree)是渲染树的一个优化版本,它考虑了元素的可见性、层叠上下文等。( 输入是
-
视图绘制模块:
- 最后,渲染引擎将布局树转换成像素,这个过程称为绘制(Painting)。绘制过程可能涉及到多个图层,这些图层最终会被发送到GPU进行渲染,以提高性能。( 输入是 reder tree渲染树,输出是layout tree布局树(精确的位置和大小))
-
整合图层成页面(Compositing) :
- 在现代浏览器中,还有一个称为合成的步骤,它将多个图层合并成一个最终的图像,然后显示在屏幕上。这个过程可以减少重绘的次数,提高性能。
-
JavaScript引擎的交互:
- 虽然在初始渲染过程中不一定需要JavaScript,但JavaScript引擎可以在任何时候修改DOM或CSSOM,触发重新渲染。
页面绘制
HTML/CSS/JS资源 -> 浏览器内核 -> 图像 parseHTML -> style 计算样式 -> layout(计算图层布局) -> paint绘制图层 -> composite整合图层成也页面
在渲染完成出界面后,将接下来的事情交给JS引擎执行,完成与用户的交互。
JavaScript引擎(V8)
JavaScript引擎专门负责执行JavaScript代码。它的主要功能是:
- 解析JavaScript代码:将JavaScript代码转换成可执行的机器代码。
- 执行代码:运行JavaScript代码,处理逻辑、事件处理等。
- 内存管理:管理JavaScript对象的生命周期和垃圾回收。
V8是最著名的JavaScript引擎之一,由Google开发,用于Chrome浏览器和Node.js环境。V8引擎以其高性能和优化的垃圾回收机制而闻名。
性能优化
JS性能提升
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<script>
for (var count = 0; count < 10000; count++){
document.getElementById('container').innerHTML +=
'<span>测试</span>'
}
</script>
</body>
</html>
过路费太多了,在两个内核走来走去,来回了10000次,更新DOM树太频繁了
document.getElementById('container')这里是JS引擎在执行
.innerHTML += '<span>测试</span>'而到了这里加入了一个DOM树的节点,交给了渲染引擎去渲染页面
为了减少过路费 把10000次字符串先在JS中拼接,最后再支付一次过路费,一次性交给渲染引擎
- 将.innerHTML拼接为一个整体
- 缓存DOM节点
<script>
let container = document.getElementById('container');
let content = ''
for (var count = 0; count < 10000; count++){
content += '<span>测试</span>'
}
// 加载DOM树 慢 只做了一次将10000个span 加载到内存中
container.innnerHTML = content
</script>
浏览器给了我们一种方法“文档碎片”
<script>
let container = document.getElementById('container');
// 文档碎片 像个伪元素,可以像真实DOM一样挂载节点
let content= document.createDocumentFragment()
for (var count = 0; count < 10000; count++){
let oSpan = document.createElement('span')//DOM节点对象
oSpan.innerHTML = '测试'
content.appendChild(oSpan)// 将span节点挂载到文档碎片上,但节点并没有挂载到页面上
}
// 慢 一次将文档碎片节点挂上去,自动消失
container.appendChild(content)
</script>
使用文档碎片可以减少页面的重绘和重排次数。而使用文档碎片,可以先在内存中构建整个节点树,然后一次性将其添加到DOM中,从而减少重绘和重排的次数。
重排和重绘
重绘(Repaint)
- 定义:当元素的样式发生变化,但这种变化不影响其在文档流中的几何尺寸(例如颜色、背景色、边框颜色等)时,浏览器需要重绘这个元素。
- 性能影响:重绘通常比重排要快,因为它只涉及到元素的视觉更新,而不需要重新计算元素的位置和大小。
重排(Reflow)
- 定义:当DOM的修改导致元素的几何尺寸发生变化(例如宽度、高度、边距、填充等),浏览器需要重新计算页面布局,这个过程称为重排或回流。
- 性能影响:重排比重绘更耗费性能,因为它不仅需要更新元素的样式,还需要重新计算页面上所有受影响元素的位置和尺寸。重排可能引起一系列的连锁反应,影响到页面上其他元素的布局。
性能优化建议
- 减少DOM操作:尽量减少对DOM的直接操作,尤其是在复杂的页面上。
- 批量修改:如果需要对多个元素进行样式修改,尽量一次性进行,以减少重排和重绘的次数。
- 使用文档片段:在对DOM进行大量修改时,可以使用
DocumentFragment来减少直接对主文档的操作。 - 使用CSS变换:对于不改变元素几何尺寸的样式变化,可以使用CSS的
transform和opacity属性,因为它们可以触发重绘而不是重排。 - 使用请求动画帧:在动画或滚动事件中,使用
requestAnimationFrame来优化重排和重绘,确保它们在浏览器的下一个重绘周期中进行。
如要进行DOM的修改时
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<script>
let container = document.getElementById('container');
container.style.width = '100px'
container.style.height = '200px'
container.style.backgroundColor = 'red'
</script>
</body>
</html>
这个代码有什么问题? 要交三次过路费,需要重新渲染三次
性能优化一下使用到CSS,将改变交给CSS,改变了的一次性交给CSS改变,再由CSS进行一次渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.show{
width: 100px;
height: 200px;
background-color: red;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
let container = document.getElementById('container');
container.classList.add('.show')
</script>
</body>
</html>
CSS性能提升
CSS选择器的方向:
- CSS选择器是从右到左进行匹配的。这意味着浏览器首先查找最具体的元素(通常是ID或类选择器),然后向上查找父元素以匹配更广泛的选择器。
举个例子:
#myList li{
list-style-type: none;
}
在这个例子中css选择器会先找到所有的li,再找到匹配的li即id="myList"中的li,这样大大减弱了性能
-
使用类选择器:
- 类选择器(如
.list-item)比标签选择器(如li)更具体,因此它们可以更快地被浏览器匹配。使用类选择器可以减少浏览器查找匹配元素所需的工作量。
- 类选择器(如
更改为如下代码可以大大提升性能。
#myList .list-item{
list-style-type: none;
}
-
避免使用通配符(*) :
- 通配符选择器会匹配所有元素,这可能导致浏览器进行大量的不必要匹配,从而降低性能。只在必要时使用通配符。
/* reset 通用样式 */
*{
margin: 0;
padding: 0;
}
更改为:
/* reset 通用样式 */
body, div, dl, dt, dd, ul, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, button, textarea, blockquote{
margin: 0;
padding: 0;
}
-
关注可继承的属性:
- 一些CSS属性(如
color、font-family等)可以被继承。在父元素上设置这些属性可以减少CSS的复杂性和提高性能,因为子元素会自动继承这些属性。
- 一些CSS属性(如
-
少用标签选择器:
- 标签选择器(如
div、p、li等)是通用的,但它们不如类或ID选择器具体。使用更具体的选择器可以减少浏览器的匹配工作量。
- 标签选择器(如
-
避免不必要的复杂选择器:
- 不要画蛇添足,如
.myList#title,因为它结合了类和ID,这可能会导致浏览器的匹配过程变得更加复杂。如果#title已经足够具体,就不需要前面的.myList。
- 不要画蛇添足,如
-
减少嵌套:
- 深层嵌套的选择器(如
#parent > .child > .grandchild)会增加浏览器的计算负担,因为它们需要逐层匹配。尽量减少嵌套的深度,或者使用更具体的类或ID选择器来替代
- 深层嵌套的选择器(如
番外思考一下
先渲染引擎工作再JS引擎工作,那么理应先写样式,再写js代码
那要是我不这么做会发生什么结果呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS阻塞</title>
<style>
#container{
width: 100px;
height: 100px;
background-color: rgb(47, 0, 255);
}
</style>
<script>
var container = document.getElementById('container')
console.log('container',container);// undefined JS引擎高于渲染引擎
</script>
</head>
<body>
<div id="container"></div>
<script>
var container = document.getElementById('container')
console.log('container',container);
</script>
</body>
</html>
在这里的头和尾都加入一段JS来输出一块<div>,得到两种不同结果
- JS引擎 霸道的,会阻塞渲染,因此在第一次输出
<div>时,渲染引擎还没有渲染到container,而输出了null,放在末尾的输出在渲染之后,于是能够输出。
理解了这个那么以下代码便能融会贯通
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS阻塞</title>
<style>
#container{
width: 100px;
height: 100px;
background-color: rgb(47, 0, 255);
}
</style>
<script>
var container = document.getElementById('container')
console.log('container',container);// undefined JS引擎高于渲染引擎
</script>
</head>
<body>
<div id="container"></div>
<script defer>
var container = document.getElementById('container')
console.log('container',container);
console.log('container backgroundColor',
getComputedStyle(container).backgroundColor
);
</script>
<style>
#container{
background-color: rgb(255, 0, 0);
}
</style>
</body>
</html>
这段代码的顺序为
- 输出尚未渲染的
container(输出null) - 渲染完成
container为蓝色方块 - 输出渲染完成的
container以及渲染的蓝色方块的颜色 - 再覆盖样式,成为红色方块
HTML <script> 标签中的 defer
- 用途: 当为外部脚本(
src属性指定的脚本)设置defer属性时,浏览器会异步下载该脚本,但会在文档解析完成后,DOMContentLoaded事件触发之前,按照脚本在文档中的出现顺序执行这些脚本。
<body>
<div id="container"></div>
<script src="demo.js" defer>
</script>
<style>
#container{
background-color: rgb(255, 0, 0);
}
</style>
</body>
JS代码放入demo.js中
var container = document.getElementById('container')
console.log('container',container);
console.log('container backgroundColor',
getComputedStyle(container).backgroundColor
);
如此这样,JS等待渲染引擎工作完之后再输出,于是输出的是红色
注意事项
defer仅对外部脚本有效,如果<script>标签没有src属性(即内联脚本),defer属性会被忽略。
defer和async同样是异步加载,但亦有区别
- defer: 浏览器会异步下载脚本,但会等到文档解析完成(即DOM构建完成)后再执行这些脚本,恰好在
DOMContentLoaded事件触发之前。这使得脚本可以在不阻塞页面渲染的同时,还能访问到完整的DOM树。 - async: 脚本同样会异步下载,一旦下载完成就会立刻执行,不必等待其他脚本或样式表。这意味着脚本执行可能在DOM构建过程中进行,因此在脚本执行时可能无法访问到完整的DOM。
总结
浏览器的底层操作是来自于渲染引擎与JS引擎的相互配合
为了让性能更佳,应该得减少两个引擎之间的“摩擦”,将所有需要的数据先在一个引擎中打包好,再一次性丢给另一个引擎执行
其次,在CSS引擎中能继续性能优化。以及尽量避免重排。