浏览器原理

81 阅读10分钟

浏览器是如何渲染页面的

当浏览器的网络线程收到 HTML 文档后,会生成一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。


整个渲染流程分为多个阶段,分别是: HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画。渲染流程都是发生在浏览器的渲染帧过程中,浏览器一般为60HZ,即16.6毫秒的时候渲染一次。

image-20230619145854885

解析HTML生成DOM树和CSSOM树 parse

为了提高解析效率,浏览器会启动一个预解析线程率先下载和解析外部的css文件,解析完成后,就会交付给浏览器的渲染主线程,生成CSSOM树。所以说,如果渲染主线程解析HTML文档过程中解析到了 link标签,此时css文件没有解析完成,渲染主线程不会等待,它会继续解析后续的HTML。这是因为css的解析工作是在预解析线程中进行的,只有等解析完成后,才会交付给浏览器的渲染主线程。这也就是为什么css不会阻塞html的解析。

HTML解析过程中遇到了js代码

渲染主线程遇到js时必须暂停一切行动,等待js下载执行完后才继续。这是因为js代码执行过程中可能会修改当前DOM树

这一步完成后,就会得到DOM树和CSSOM树

image-20230619110619574

样式计算 style

生成DOM树后,主线程会遍历 DOM 树并结合CSSOM树,依次为树中的每个节点计算出它最终的样式,比如em会变成 pxred会变成rgb(255,0,0),这一步完成后,会得到一棵带有样式的 DOM 树,称为render树

布局 layout

渲染树生成后,接下来就是布局,布局阶段会依次遍历 render树(带有样式的DOM树)的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置

大部分时候,DOM树和布局树不是一一对应的。比如display:none的DOM节点并不会作为布局树节点添加到布局树中。

分层 layer

主线程会使用一套复杂的策略对整个布局树中进行分层,和堆叠上下文有关的属性如z-index,opacity会影响到分层的结果。分层的好处在于,将来某一个层改变后,仅会对该层进行处理,从而提升效率。

绘制 paint

这里的绘制,是为每一层生成如何绘制的指令用于描述这一层的内容该如何画出来。其实类似于canvas

分块

分块会将每一层分为多个小的区域,将工作交给多个线程同时进行。

光栅化

分块完成后,合成线程会将块信息交给GPU进程进行光栅化即将每个块变成位图,它会优先处理靠近视口的块

合成线程拿到每个块的位图后,生成一个个「指引(quad)」信息。指引会标识出每个位图怎么画。transform就是发生在这里,它与之前的步骤(样式计算,布局,分层绘制指令无关),与渲染主线程无关,只是个矩阵变换,所以效率高

什么是reflow (回流 重排)

reflow 的本质就是重新计算 layout 树,即重新布局。它会影响到后面的分层,绘制,分块,光栅化,画等多个步骤。

当进行了会影响布局树的操作后比如修改了width,padding,height等,需要重新计算布局树,会引发 layout重新布局,继而影响到后面的多个步骤。

为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的

当 JS 获取布局属性clientWidthoffsetWidth时,就可能造成无法获取到最新的布局信息,因此会立刻reflow。

reflow一定会导致repaint

什么是repaint (重绘)

修改了颜色、透明度等,与几何信息无关的属性,它只需要重新计算样式,无需重新布局,也无需分层,但是需要paint,分块,光栅化和画。

为什么transform效率高

因为transform发生在最后一个draw步骤中,而且与渲染主线程无关,只是个矩阵变化,所以效率高。

事件循环

浏览器的进程模型

何为进程?

程序运行需要专属的内存空间,可以把这块内存空间简单的理解为进程。

因此,官方话来说:进程是资源分配的最小单位(分配的就是内存空间)。

每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意

何为线程?

有了进程后,就可以运行执行程序的代码。

运行代码的[人]可以称为线程。

因此,官方话来说:线程是操作系统可调度的最小单位,也是实际执行代码的单位

一个进程至少有一个线程。所以在进程开启后回自动创建一个线程来运行代码,该线程称为主线程。

如果程序需要同时执行多块代码,主线程会创建更多的线程来执行代码,所以一个进程中可以包含多个线程。

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

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

多线程可以理解,因为需要执行大量代码。

为何是多进程呢?

浏览器内部工作及其复杂,为了避免浏览器内部各个模块相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。

常见的浏览器的主要进程:

  1. 浏览器主进程。

    负责协调管理其他进程,并处理用户输入事件等等。

  2. 网络进程

    负责加载网络资源,请求HTML文档等等。

  3. 渲染进程

    负责:解析和渲染网页内容。

    渲染进程启动后,会开启一个渲染主线程,负责执行HTML、CSS、JS代码。

    默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以避免不同的标签页之间相互影响。(进程之间相互隔离,互不干扰)

  4. GPU进程

    作用:加速图形渲染,减轻渲染进程的负担

  5. 插件进程

    作用:负责加载插件

渲染进程中的线程:

  • 渲染主线程渲染主线程是渲染进程的核心, 负责解析HTML、CSS和JS、构建DOM树、CSSOM树,负责执行JS代码。
  • 网络线程:负责发出网络请求,加载资源
  • 定时器线程:负责处理定时器

渲染主线程如何工作的?

渲染主线程是最繁忙的线程:

  • 解析HTML

  • 解析CSS

  • 计算样式

  • 布局

  • 每秒把页面画60次 即渲染帧绘制

  • 执行全局js代码

  • 执行事件处理函数

  • 执行计时器的回调函数

    ....

要处理这么多任务,渲染主线程遇到了一个前所未有的难题:如何调度任务?

比如:

  • 正在执行一个js函数,用户点击按钮,你该立即去执行按钮的回调函数吗
  • 正在执行一个js函数,定时器到达了时间,应该立刻执行吗?

渲染主线程想出了一个绝妙的主意:排队

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

整个过程,就被称为事件循环

何为异步?

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

  • 计时完成后需要执行的任务 ——setTimeout、setInterVal
  • 网络通信 —— XHR、Fetch
  • 事件 —— addEventListener

如果让渲染主线程等待这些任务的时机到来,就会导致主线程长期处于 阻塞状态(主线程需要解析HTML、需要1s画60次页面等等,很忙!!!),从而导致浏览器卡死。

所以主线程无论如何都不能阻塞!!!

因此,浏览器使用异步的方式,渲染主线程永不阻塞

比如计时器:①渲染主线程通知计时线程计时,当前任务结束。②渲染主线程获取下一个任务继续执行③计时任务结束后,计时线程将回调函数放入到事件队列末尾。

面试题:如何理解js的异步

面试题:如何理解js的异步

js是一门单线程语言,它运行在浏览器的js引擎线程中。

如果js代码只使用同步的方式,就极有可能导致js引擎线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的js引擎线程白白的消耗时间,另一方面导致页面无法及时更新,造成卡死。

所以浏览器采用异步的方式来避免,当js引擎线程遇到了异步任务,如定时器、网络、事件监听,js引擎线程将任务交给其他线程去处理,自身立即结束当前任务的执行继续执行下一个任务,当其他线程完成时,再将回调函数加入到消息队列的末尾,等待js引擎线程调度执行。在这种异步模式下,最大限度的保证了单线程的流畅执行。

任务有优先级吗?

任务没有优先级,任务谁先来谁就执行,但是消息队列有优先级

过去,使用的是宏任务和微任务两个概念。现在分为:

  • 延时队列:用于存放计时器时间到达后的回调函数。优先级
  • 交互队列:用于存放用户操作后产生的事件处理任务。优先级
  • 微任务。promise.then的回调函数 、MutationObserver 。优先级最高

注意:必须全局任务或者函数内局部任务执行完成后,才会对三个任务队列排序执行。

面试题:阐述一下js的事件循环

事件循环又称为消息循环,是浏览器渲染线程的工作方式。它会不断地从消息队列中取出第一个任务,添加到执行栈中执行。当执行栈为空时,它会进入等待状态,等待其他线程将任务添加到消息队列中,再循环执行。

过去消息队列只是简单的分为宏任务和微任务。宏任务比如事件处理,script标签导入,计时器。微任务MutationObserver,promise.then的回调函数。总体流程为:首先执行一个宏任务,执行过程中如果遇到了微任务将微任务推入到微任务队列中,执行完当前任务后,如果微任务队列中有任务,就根据先后顺序依次执行所有的微任务。当所有微任务完成后,再执行下一个宏任务。

现在w3c分的更加精细:分为延时队列、交互队列和微任务。微任务的优先级最高其次交互任务最后延时任务。