浏览器底层操作,渲染引擎与JS引擎的相互配合,再聊聊性能优化

331 阅读10分钟

浏览器底层操作

在浏览器技术中,渲染引擎JavaScript引擎是两个重要的组成部分,它们在前端和后端开发中扮演着不同的角色。

渲染引擎

浏览器的内核,也被称为渲染引擎,是浏览器的核心组件,负责解析网页的HTML、CSS和JavaScript代码,并将其渲染成用户可以交互的网页。不同的浏览器可能使用不同的内核。

不同的浏览器使用不同的渲染引擎,如:

  • WebKit/Blink:用于Safari和Chrome。
  • Gecko:用于Firefox。
  • Trident:早期的Internet Explorer。
  • EdgeHTML:旧版Microsoft Edge。

渲染引擎的工作原理

  • 渲染引擎是怎么工作的?

渲染引擎的工作流程可以分为几个关键步骤

  1. HTML解析器(DOM Parser)

    • 渲染引擎首先解析HTML文档,生成DOM树(Document Object Model Tree)。DOM树是网页结构的树状表示,每个节点代表一个HTML元素。(html -> DOM树)
  2. CSS解析器

    • 然后,渲染引擎解析CSS样式表,生成CSSOM树(CSS Object Model Tree)。CSSOM树是样式规则的树状结构,每个节点包含CSS选择器和相应的样式属性。
  3. 合并生成渲染树(Render Tree)

    • 渲染引擎将DOM树和CSSOM树结合起来,创建渲染树。渲染树包含了所有需要在屏幕上显示的元素,以及它们的样式信息。(DOM树和CSSOM树合并 -> 渲染树)
  4. 图层布局计算模块

    • 渲染树生成后,渲染引擎会计算每个节点的精确位置和大小,这个过程称为布局(Layout)或重排(Reflow)。布局树(Layout Tree)是渲染树的一个优化版本,它考虑了元素的可见性、层叠上下文等。( 输入是 render tree渲染树,输出是layout tree布局树(精确的位置和大小))
  5. 视图绘制模块

    • 最后,渲染引擎将布局树转换成像素,这个过程称为绘制(Painting)。绘制过程可能涉及到多个图层,这些图层最终会被发送到GPU进行渲染,以提高性能。( 输入是 reder tree渲染树,输出是layout tree布局树(精确的位置和大小))
  6. 整合图层成页面(Compositing)

    • 在现代浏览器中,还有一个称为合成的步骤,它将多个图层合并成一个最终的图像,然后显示在屏幕上。这个过程可以减少重绘的次数,提高性能。
  7. 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的transformopacity属性,因为它们可以触发重绘而不是重排。
  • 使用请求动画帧:在动画或滚动事件中,使用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,再找到匹配的liid="myList"中的li,这样大大减弱了性能

  1. 使用类选择器

    • 类选择器(如.list-item)比标签选择器(如li)更具体,因此它们可以更快地被浏览器匹配。使用类选择器可以减少浏览器查找匹配元素所需的工作量。

更改为如下代码可以大大提升性能。

#myList .list-item{
    list-style-type: none;
}
  1. 避免使用通配符(*)

    • 通配符选择器会匹配所有元素,这可能导致浏览器进行大量的不必要匹配,从而降低性能。只在必要时使用通配符。
/* 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;
}
  1. 关注可继承的属性

    • 一些CSS属性(如colorfont-family等)可以被继承。在父元素上设置这些属性可以减少CSS的复杂性和提高性能,因为子元素会自动继承这些属性。
  2. 少用标签选择器

    • 标签选择器(如divpli等)是通用的,但它们不如类或ID选择器具体。使用更具体的选择器可以减少浏览器的匹配工作量。
  3. 避免不必要的复杂选择器

    • 不要画蛇添足,如.myList#title,因为它结合了类和ID,这可能会导致浏览器的匹配过程变得更加复杂。如果#title已经足够具体,就不需要前面的.myList
  4. 减少嵌套

    • 深层嵌套的选择器(如#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>,得到两种不同结果

ee09b34775c01b3897cd62fbd1de53b.png

  • 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以及渲染的蓝色方块的颜色
  • 再覆盖样式,成为红色方块 98ce3285218130227bb335df4414f1f.png

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
        );

7af5b1d26b681c3bab00194ed2ce66c.png 如此这样,JS等待渲染引擎工作完之后再输出,于是输出的是红色

注意事项

  • defer仅对外部脚本有效,如果<script>标签没有src属性(即内联脚本),defer属性会被忽略。

deferasync同样是异步加载,但亦有区别

  • defer: 浏览器会异步下载脚本,但会等到文档解析完成(即DOM构建完成)后再执行这些脚本,恰好在DOMContentLoaded事件触发之前。这使得脚本可以在不阻塞页面渲染的同时,还能访问到完整的DOM树。
  • async: 脚本同样会异步下载,一旦下载完成就会立刻执行,不必等待其他脚本或样式表。这意味着脚本执行可能在DOM构建过程中进行,因此在脚本执行时可能无法访问到完整的DOM。

总结

浏览器的底层操作是来自于渲染引擎与JS引擎的相互配合

为了让性能更佳,应该得减少两个引擎之间的“摩擦”,将所有需要的数据先在一个引擎中打包好,再一次性丢给另一个引擎执行

其次,在CSS引擎中能继续性能优化。以及尽量避免重排。