前端-浏览器原理

135 阅读21分钟

1. 浏览器的进程模型

浏览器是一个多进程多线程的应用程序

为了避免相互影响,每个进程都有自己的内存空间,减少连环崩溃的几率。

image-20220809213152371

可以在浏览器的任务管理器中查看当前的所有进程,谷歌浏览器快捷键:Shift+Esc

2. 浏览器有哪些进程和线程?

image.png 其中,最主要的进程有:

  1. 浏览器主进程(Browser Process)

    • 主要负责tab页的管理
    • 控制浏览器的主窗口和各个子进程的创建和销毁
    • 管理用户界面、存储缓存和历史记录等功能
  2. 渲染进程(Renderer Process)-- 重点

    • tab页都会有一个独立渲染进程(防止一个网页的崩溃引起整个浏览器的崩溃)
    • 负责界面渲染工作,解析HTML、CSS、图片渲染
    • JavaScript 解析和执行:解析和执行页面中的 JavaScript 代码,处理事件响应、动态内容生成等交互操作。
  3. GPU进程(GPU Process)

    • 处理网页中的图像和视频,使用浏览器的硬件加速技术实现的
    • 处理复杂的三维图形场景(3D)、CSS3等等视觉效果
  4. 网络进程(Renderer Process)

    • 网络进程的工作是实现浏览器与服务器之间的网络通信。
    • 负责发送和接收网络请求和响应,解析 URL,处理安全性检查,管理 Cookie 和缓存,处理跨域请求,以及进行数据传输和流控制
  5. 插件进程(Plug-in Process)

    • 插件运行在插件进程,每个不同的插件都会运行在一个新的进程

将来该默认模式可能会有所改变,有兴趣的同学可参见chrome官方说明文档

3. 渲染进程

image.png

主要线程如下:

  1. 主线程(Main Thread)

    • 渲染主线程是单线程
    • 处理用户交互、解析 HTML、构建 DOM 树、进行布局和绘制等任务。
  2. 渲染线程(Renderer Thread)

    • 渲染线程负责将 HTML、CSS 和 JavaScript 转换为可视化的页面
    • 负责布局(Layout)、绘制(Painting)和合成(Compositing)等任务,渲染线程是渲染进程中的辅助线程,负责执行部分与渲染相关的任务
  3. JavaScript 引擎线程(JavaScript Engine Thread)

    • 解析和执行 JavaScript 代码。
    • 常见的 JavaScript 引擎有 V8(用于 Chrome)、SpiderMonkey(用于 Firefox)和 JavaScriptCore(用于 Safari)等。
  4. 事件线程(Event Thread)

    • 处理用户输入、网络请求、定时器等异步事件。将接收的事件分发给适当的线程进行处理。
  5. 定时器线程(Timer Thread)

    • 定时器线程负责管理 JavaScript 中的定时器,当定时器到期时,定时器线程会将相应的回调函数添加到事件队列中,等待事件线程执行。
    • 就是给定时任务计时,例如5秒针后执行exec()方法,当5秒针结束后,将exec()方法放入事件队列中。
  6. 异步 HTTP 请求线程(Asynchronous HTTP Request Thread)

    • 如果页面中存在异步的 HTTP 请求(例如 AJAX 请求),则这些请求通常在单独的线程中执行,以防止阻塞主线程。
    • 异步 HTTP 请求线程和浏览器的网络进程是协同工作的,当渲染进程需要发送异步的 HTTP 请求时,它会通过与网络进程之间的通信机制,将请求的详细信息传递给网络进程。网络进程接收到请求后,会执行实际的网络操作,包括建立连接、发送请求、接收响应等。一旦响应返回,网络进程将响应数据传递给渲染进程的异步 HTTP 请求线程,以触发相应的回调函数。

3.1 渲染主线程

渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画 60 次
  • 执行全局 JS 代码
  • 执行事件处理函数
  • 执行计时器的回调函数
  • ......

渲染主线程为单线程

  • 原因:

    ​ 确保页面的一致性,浏览器渲染主线程负责处理网页的渲染和用户交互,包括 DOM 操作、样式计算、布局和绘制等。

    ​ 如果允许多个线程同时修改页面的状态,可能会导致页面的不一致性,例如不同线程对同一元素进行不同的操作,可能会导致渲染冲突和显示错误。

3.1.1 调度任务一般原则

任务调度是由浏览器的渲染引擎负责的。任务调度的目标是确保页面的渲染和用户交互的响应性,并尽可能平衡各种任务的执行。

渲染主线程中任务调度的一般原则:

  1. 任务队列(Task Queue)

    • 渲染主线程会维护一个任务队列,用于存储各类任务
    • 任务队列中的任务可以是浏览器事件(如点击事件、滚动事件)、网络请求的回调、定时器任务等。
    • 每个任务都有一个优先级,用于确定任务的执行顺序。
    • 队列分类很多个,用来存储不同的任务类型。
  2. 事件循环(Event Loop)

    • 渲染主线程通过事件循环机制来处理任务队列中的任务
    • 事件循环不断地从任务队列中取出任务并执行,直到任务队列为空。
  3. 响应优先级

    • 渲染主线程会根据任务的优先级来调度任务的执行顺序
    • 通常,用户交互相关的任务(如点击事件)具有较高的优先级,以保证用户操作的响应性。
    • 而低优先级的任务(如定时器任务)可能会被延迟执行,以避免阻塞高优先级任务的执行。
  4. 分片和时间片

    • 为了保证页面的渲染和用户交互的流畅性,渲染主线程会将长时间运行的任务分割成较小的片段,并在执行每个片段后,让出执行权给其他任务。
    • 这样可以避免长时间运行的任务阻塞渲染主线程,保持页面的响应性。
  5. 帧率控制

    • 渲染主线程还会根据显示设备的刷新率(通常为 60 帧/秒)来控制任务的执行。
    • 它会尽量在每个刷新周期内完成渲染和绘制操作,以保证页面的流畅性。
    • 如果某个任务无法在一个刷新周期内完成,渲染主线程会将其延迟到下一个刷新周期。

3.1.2 调度任务流程

image-20220809223027806

  1. 在最开始的时候,渲染主线程会进入一个无限循环(事件循环)
  2. 每一次循环会检查消息所有队列中是否有任务存在。如果有,就取出任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到对应消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务

整个过程,被称之为事件循环(消息循环)

3.1.2 异步任务调度

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 计时完成后需要执行的任务 —— setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 -- XHRFetch
  • 用户操作后需要执行的任务 -- addEventListener

为了保证渲染主线程不阻塞!使用了定时器线程处理异步任务

image-20220810104858857

  1. 当遇到异步任务时,浏览器不会立即执行它,将其放入计时线程中。
  2. 计时结束后将它放入任务队列(Task Queue)的最末尾。
  3. 浏览器会不断地进行事件循环,从任务队列中取出一个任务并执行。这个过程会重复进行,直到任务队列为空。

3.1.3 任务优先级

在浏览器中,任务可以分为以下几种类型,按照执行优先级从高到低排列:

  1. 宏任务(Macro Task):

    • 宏任务包括整体代码块(例如整个 script 标签中的代码)、setTimeout/setInterval(延时队列)、I/O 操作、UI 渲染等。
    • 宏任务的执行优先级较低,会在微任务执行完毕后执行。
  2. 微任务(Micro Task):

    • 微任务包括Promise回调、MutationObserver回调等。
    • 微任务具有高优先级,会在宏任务执行完毕、页面渲染之前执行。
  3. 动画帧(Animation Frame):

    • 动画帧任务用于执行与页面动画相关的操作,例如使用 requestAnimationFrame 方法注册的回调函数。
    • 动画帧任务的优先级介于宏任务和微任务之间。
  4. 交互任务(User Interaction):

    • 交互任务用于处理用户交互事件,例如点击、滚动等。
    • 交互任务的优先级通常高于宏任务和微任务,但低于动画帧任务。
  5. 渲染任务(Rendering):

    • 渲染任务用于执行页面渲染相关的操作,例如绘制图形、计算布局等。
    • 渲染任务的优先级通常较低,会在其他任务执行完毕后进行。

执行优先级总结(从高到低):

  1. 微任务(Micro Task)
  2. 动画帧(Animation Frame)
  3. 交互任务(User Interaction)
  4. 宏任务(Macro Task)
  5. 渲染任务(Rendering)

注意:

  • 不同浏览器可能对任务类型和执行优先级有所调整
  • 事件循环取宏任务和渲染任务的队列有帧率控制,渲染时每16.6毫秒为一帧,在这一帧的中间可以穿插部分宏任务,避免宏任务队列过多导致不执行渲染任务。

事件循环的宏任务和渲染时机逻辑

for(;;){
    取出宏任务;
    执行宏任务
    if(渲染时机是否到达){
        渲染
    }
}

在目前 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列:用户存放需要最快执行的任务,优先级「最高」

添加任务到微队列的主要方式主要是使用 Promise、MutationObserver

例如:

// 立即把一个函数添加到微队列
Promise.resolve().then(函数)

4. 页面渲染

4.1 下载流程

image.png 由网络进程下载HTML文件,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。 在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。

4.2 渲染流程

image.png

  1. 解析 HTML
    • 当用户访问一个网页时,浏览器会下载对应的 HTML 文件。
    • 将HTML文件解析为 DOM(文档对象模型)树的结构,DOM 树表示了 HTML 文档的结构和内容。
  2. 构建 CSSOM
    • 同时,浏览器也会下载对应的 CSS 文件,并开始解析。
    • 将 CSS 解析成 CSSOM(CSS 对象模型),CSSOM 表示了页面中所有样式规则的层级结构和继承关系。
  3. 创建渲染树
    • 浏览器将 DOM 树和 CSSOM 结合起来,创建渲染树(Render Tree)
    • 渲染树只包含需要显示的元素节点(例如 div、p 等),并且考虑了每个元素节点的样式信息。
  4. 布局(Layout)
    • 布局过程确定了每个渲染树节点在屏幕上的位置和大小。
    • 浏览器会计算每个元素的盒模型(Box Model),包括宽度、高度、边距、填充等属性。
  5. 绘制(Painting)
    • 浏览器使用布局计算出来的信息,将渲染树节点绘制到屏幕上。
    • 绘制过程将渲染树节点转换为像素,使用图形库将像素绘制到屏幕上。
  6. 合成(Composition)
    • 如果页面中存在多个图层(例如有定位、透明度等效果的元素),浏览器会对些图层进行合成
    • 合成过程将图层按照正确的顺序进行组合,以提高渲染性能。
  7. 显示页面
    • 浏览器将渲染结果显示在用户的屏幕上,用户可以看到完整的页面。
  • 当浏览器渲染网页时,会将 DOM 树和 CSSOM 树合并成一个 Render Tree(渲染树),然后进行布局和绘制,最终呈现在用户界面上。
  • 渲染树只包含需要显示的元素节点,并且考虑了每个元素节点的样式信息。由于渲染树中只包含需要显示的元素节点,因此它的结构可能与 DOM 树不完全一致,例如隐藏的元素和无需显示的节点可能会被从渲染树中移除

4.2.1 DOM 树

表示 HTML 文档结构的一种树形结构,它将 HTML 文档中的每个标签以及标签之间的关系转化为一个节点树形结构。每个 HTML 标签都对应 DOM 树中的一个节点,包括文本节点、元素节点、属性节点等。DOM 树的根节点是文档节点,表示整个 HTML 文档,而子节点则对应 HTML 中的标签和属性。

查看方式

  • 方式一:开发者工具,Elements(元素)选项卡就是DOM 树。
  • 方式二:document.documentElement可以获取DOM树。

4.2.2 CSSOM 树

是表示 CSS 样式信息的一种树形结构,它将 CSS 文件中的每个样式规则转化为一个节点树形结构每个样式规则都对应 CSSOM 树中的一个节点,包括规则节点、选择器节点、样式属性节点等。CSSOM 树的根节点是样式表节点,表示整个 CSS 文件,而子节点则对应 CSS 中的选择器和样式属性。

查看方式

  • 方式一:浏览器F12打开开发者工具,可以在 Elements 选项卡右侧的 Styles(样式)面板就是CSSOM 树。
  • 方式二:js:document.styleSheet可以获取CSSOM树。

4.3 解析HTML和CSS

image.png

  1. 下载HTML文件,从上往下依次解析。
  2. 解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件
  3. 解析过程遇到CSS引用解析CSS,遇到JS引用执行 JS
    • 如果主线程解析到link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。
    • 如果主线程解析到script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。

当步骤完成后会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。

注意:

  • css的加载是会阻塞后续js的执行的,后续js会等待css加载完成后才会执行
  • css的加载并不会阻塞Dom Tree的构建
  • css的加载是会阻塞页面渲染的,因为页面渲染的Render Tree(渲染树)是需要css omdom tree进行合并从而渲染页面的。

4.4 生成渲染树(Render Tree)

样式计算。 样式计算是在生成 CSSOM 树过程中进行,主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。完成后,会得到一棵带有样式的 DOM 树。

比如:很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px

查看: 浏览器F12打开开发者工具,可以在 Elements 选项卡右侧的Computed面板就是计算之后的最终样式

在样式计算完成后,将 DOM 树和 CSSOM 树进行合并,生成渲染树(Render Tree)。渲染树只包含需要显示在页面上的可见元素节点。

4.5 布局

布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置

DOM 树和布局树并非一一对应。

比如display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。

4.6 分层和分块

分层(Layering)是在布局(Layout)阶段之后进行的。 在布局完成后,渲染引擎根据渲染树的层叠顺序和一些优化策略,将渲染树中的元素节点进行分层操作。分层的目的是为了在后续的绘制阶段提供更高效的渲染操作,将不同层级的元素节点进行离屏渲染或合并渲染。

分层好处

分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。

列如:滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。

查看分层

打开开发者工具后,最右侧有三个小点--->More tools--->Layers

image.png 分块(Chunking)是在布局(Layout)后并行进行的优化步骤。它将绘制区域划分为多个块,每个块单独执行绘制操作。这样可以通过并行计算和渲染,提高渲染性能和响应速度。

image.png

4.7 绘制

image.png 在分层和分块之后,将渲染树中的元素节点转换为图像或位图,并进行实际的视觉绘制操作。

绘制过程包括填充颜色、绘制边框、渐变、图片等,以生成最终的可视化效果。

完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。

合成线程首先对每个图层进行分块,将其划分为更多的小区域。

它会从线程池中拿取多个线程来完成分块工作。


分块完成后,进入光栅化阶段。 在绘制完成后,将绘制得到的图像分割为小块,生成栅格(Raster)或位图数据。这个过程通常涉及将矢量图形转换为像素图形,以便在显示设备上进行显示。

合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。

GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。

光栅化的结果,就是一块一块的位图


最后一个阶段就是

合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。

变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。

合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

4.8 重新渲染

当JavaScript动态改变了页面或者CSS有动画效果时,页面重新渲染的流程如下:

  1. 页面的DOM(文档对象模型)或者 CSSOM树 发生了变化,比如修改了某个元素的内容、样式或属性等。
  2. 浏览器检测到变化,触发重绘(reflow)或重排(repaint)事件。
  3. 浏览器开始重新构建渲染树(Render Tree),根据新的DOM树和CSSOM树生成新的渲染树。
  4. 根据新的渲染树,浏览器计算出每个元素的新位置和大小,形成新的布局。
  5. GPU线程根据新的布局信息,绘制页面中的每个元素,完成页面的重新渲染。

需要注意的是,重绘和重排是两个不同的概念:

  • 重排(reflow):指浏览器重新计算元素的位置和大小的过程,它是必需的,因为DOM的改变可能影响到其他元素的布局。重排通常会导致页面尺寸的变化。
  • 重绘(repaint):指浏览器重新绘制元素的过程,包括背景色、文字颜色、边框颜色等。重绘通常不会影响页面的布局。

在现代浏览器中,为了提高渲染效率,一些优化技术被引入,例如异步渲染、分块渲染、使用requestAnimationFrame等技术。这些技术可以减少重排和重绘的次数,从而提高页面的渲染速度和性能。

4.8.1 触发reflow和repaint条件

Reflow是指浏览器根据新的CSS样式重新计算页面元素的位置和大小的过程。以下是一些会导致reflow发生的情况:

  1. 添加或删除样式:当添加或删除样式时,浏览器必须重新计算页面的布局和元素的位置。
  2. 内容的改变:当页面的内容发生改变时,比如用户在输入框中输入文字,浏览器必须重新计算页面的布局和元素的位置。
  3. 激活伪类:当激活如hover、active等伪类时,浏览器必须重新计算页面的布局和元素的位置。
  4. 操作class属性:当通过操作class属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置。
  5. 脚本操作DOM:当使用JavaScript来操作DOM时,浏览器必须重新计算页面的布局和元素的位置。
  6. 设置style属性:当通过设置style属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置。

Repaint是指浏览器根据新的CSS样式重新绘制页面元素的过程。以下是一些会导致repaint发生的情况:

  1. 改变窗口大小:当窗口大小改变时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
  2. 改变文字大小:当文字大小改变时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
  3. 激活伪类:当激活如hover、active等伪类时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
  4. 操作class属性:当通过操作class属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
  5. 脚本操作DOM:当使用JavaScript来操作DOM时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
  6. 设置style属性:当通过设置style属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。

总之,repaint和reflow是不可避免的,只能尽可能地减少它发生,以提高页面的性能和用户体验。

为什么 transform 的效率高?

因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段

由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。