浏览器呈现页面的过程(二)

当浏览器为用户呈现一个页面时,这些进程与线程之间是如何通信的?
以一个常见的例子作为起点:输入一个url,浏览器会从服务端获取数据并将页面展示出来。

1.用户通过浏览器向一个站点发起访问请求以及浏览器准备渲染这个页面的部分,这个过程称之为导航。

在这里插入图片描述

Step1: 处理用户输入
浏览器的地址栏同时还是一个搜索框,当用户开始在地址栏输入时,UI线程需要解析用户的输入,才能决定是直接访问网址还是把用户的输入给搜素引擎处理。

Step2:开始导航
当用户按下回车键后,UI线程要求网络线程去获取网站的内容。窗口的Tab上会开始转菊花,网络线程会采用一系列的协议和操作(比如DNS)查询必要的信息并为请求建立连接。网络线程可能会收到来自服务器的一个标记着重定向指令的头部比如 HTTP 301,在这种情况下,网络线程会把这件事情告诉UI线程,之后则会发起一次指向重定向地址的新的网络请求。

Step3:读取响应
当响应的数据开始传送到浏览器时,网络线程会在必要的情况下检查一些来自响应的字段。响应数据的Content-Type字段会表示当前返回的是哪种类型的数据,但它也不完全靠谱,经常会出现丢失或者干脆不准确的情况。MIME嗅探会完成确实的工作。如果响应数据是一个HTML文件,那么接下来的一步是把数据传递给浏览器的渲染进程;但如果数据是zip压缩文件或其他类型的文件,意味着着将被定位成一次下载动作,于是浏览器会将数据转交给下载管理器去处理。

通常这一步也是安全检测发生的时候:如果域名或响应数据和已知的恶意网站匹配时,网络进程会抛出一个警告,并展现一个警告的页面。另外CORB检测也会开始工作,确保那些来自敏感站点的跨站响应数据不会进入到浏览器的渲染进程中。

Step4:渲染进程
网络线程以获取了全部的数据,并完成了所有需要的检查,网络线程通知UI线程数据已经准备好了,UI线程会唤起一个渲染进程去渲染页面。

由于网络情况的不可控,一个请求可能会花上好几百毫秒才能把响应数据拿回来,所以浏览器默认开启了用来加速这一过程的优化。在Step2中,当UI线程将需要请求的url告诉网络线程时,其实它本身已经知道要导航到哪个网站了,于是UI线程在把url传递给网络线程的同时,会尝试启动一个渲染进程,如果一切都按照预期正常进行的话,当网络线程拿到数据时,渲染进程就已经处于待命状态了。

Step5:触发导航
现在假设数据和渲染进程都准备好了,浏览器进程通过IPC告知渲染进程可以触发本次导航了。与此同时,数据流也将传递给渲染进程,这样渲染进程就能继续接收HTML数据。一旦浏览器进程收到了来自渲染进程的导航启动信号,这次导航也就完成了,下一步进入文档的加载阶段。

到这会儿,浏览器的地址栏更新,安全指示符和站点的设置UI会将新页面的信息呈现出来。当前窗口的session将会更新,刚导航到的页面会被后退/前进按钮记录到窗口的页面历史中。为了方便在关闭窗口时恢复页面,历史的会话记录会保存在本地磁盘上。

Extra Step:初始加载完成
当导航出发后,渲染进程会持续接收资源并渲染页面。当渲染进程“完成”渲染后,它会通过IPC告知浏览器进程,UI线程也就不再在Tab上转菊花了。

上面的“完成”两个字,之所以打了双引号,因为在实际场景中,它通常并不意味着完成,因为客户端的JavaScript可能在此时持续地加载资源并渲染新的视图。

渲染进程的内部机制
https://frarizzi.science/journal/web-engineering/browser-rendering-queue-in-depth
https://frarizzi.science/journal/web-engineering/javascript-main-thread-dissected

渲染进程中两块主要的功能:
1.UI渲染
2.JS的执行

渲染进程处理Web页面的所有内容
一个浏览器窗口之内发生的所有事情,都是被渲染进程所掌握着的。前端工程师门的代码由渲染进程中的主线程处理。Compositor线程和Raster线程也运行在渲染进程中,他们的作用是高效平滑的渲染出一个页面。

渲染进程最核心的工作是:将HTML,CSS和Javascript代码变成一个可与用户交互的Web页面。

解析文档
构建DOM树
当渲染进程接收到一条即将去导航的信号并开始接收HTML数据时,主线程就开始了自己的工作:解析HTML文本并将其转换为文档对象模型。

DOM是浏览器内部对一个页面的抽象,也是开发者利用JavaScript与之相交互的数据结构和API。

子资源加载
一个网站通常会用到很多外部资源,比如图片、CSS和JavaScript。这些文件都需要从网络或是缓存中加载。当主线程在解析HTML文档的时候发现了这些额外加载的资源,主线程可以逐个请求它们。为了提速,同时运行“预加载扫描器”,如果文档中存在img标签或是link标签,预加载扫描器会“窥探”到HTML解析器生成的token,并向浏览器进程中的网络线程发起请求。

JavaScript阻塞解析
当HTML解析器遇到了Script标签时,它会暂停对HTML的解析工作,转而去加载、解析并执行JavaScript代码。因为JavaScript可能改变文档的结构,因此HTML解析器必须在JavaScript执行过后才恢复对HTML文档的解析工作。

可以在Script标签上加上async/defer属性,浏览器就会异步地加载并执行JavaScript代码并且不会阻塞对于文档的解析。也可以用到JavaScript模块。

样式的计算
只有DOM是无法知道一个页面最终会是什么样子的,因此我们还需要CSS。主线程在完成对CSS的解析和计算后,才会为每个DOM节点赋予最终的样式。

布局
现在渲染进程知道了文档的结构和每个节点的样式,但这还不足以去渲染一个页面。布局是查找元素几何形状的过程。主线程遍历DOM并计算样式,创建一个具体横坐标以及盒子边界大小数据的布局树。布局树可能和DOM树相似,但它只包含页面即将呈现的节点相关的信息。

如果某个元素设置了display:none,虽然它会呈现在DOM树中但并不会包含于布局树中;如果有一个伪类元素p::before{ content: ‘’ },它虽然不会出现在DOM树中,但仍然会出现在布局树当中。

绘制
拥有DOM,样式和布局仍然不足以呈现页面。假设您正在尝试复制一幅画。您知道元素的大小,形状和位置,但是仍然需要判断以什么顺序绘制它们。

在这一绘制过程中,主线程遍历布局树从而去创建绘制记录。绘制记录是绘制进程的“笔记”,记下了诸如“先是背景,然后文字,接下来是矩形”这样的记录。

更新渲染的代价是很大的
在渲染中要掌握的最重要的事情是,在每个步骤中,先前操作的结果都用于创建新数据。例如,如果布局树中发生某些更改,则需要为文档的受影响部分重新生成“绘制”顺序。

如果页面上有动画元素,浏览器会在每一帧都执行这些操作。大多数显示器会以每秒60次的频率刷新屏幕,当你以这样的运动速率去维持动画时,人眼对于动画的感知会是流畅的。然而,如果动画“丢帧”了,页面就会看起来很不友好。

即使你的渲染操作跟上了屏幕的刷新频率,但这些计算始终是运行在主线程上的,这就意味着当你的应用在运行JavaScript时,这些都会被阻塞掉。

可以将JavaScript的操作分割为许多小的块并放在requestAnimationFrame里执行。

合成
现在浏览器知道了文档的结构、每个元素的样式、页面的几何构成以及绘制的顺序,将这些信息转化为屏幕上的像素,这个过程叫做光栅化。

合成是一种将页面的各个部分分为多层,分别对其进行栅格化并在称为合成器线程的单独线程中作为页面进行合成的技术。如果发生了滚动,因为每一层都已经完成了光栅化,剩下需要做的就只是合成出一个窗口。可以通过移动图层并合成新帧来以相同的方式实现动画。

分层
为了确定每个元素各自应该在哪一层,主线程在遍历了布局树后生成了一个Layer Tree,主线程会将这个消息提交给合成线程。然后,合成器线程将每个图层光栅化。一个图层有可能和整个页面一样大,所以合成器线程将他们切割为很多小块,并将这些块发送给光栅线程。光栅线程将一小块完成光栅化后将其保存在GPU的内存当中。
光栅化后,合成器线程将收集称为绘制四边形(Draw Quads)的图块信息以创建合成帧框架(Compositor frame),合成帧会通过IPC被传递给浏览器进程。合成帧被运送至GPU,目的就是为了显示在屏幕上。如果这时滚动页面,合成器线程会创建新的合成帧并将之发送到GPU。
合成的优势是所有的合成操作都是独立于主线程进行的。合成器线程不需要等待样式的计算或是JavaScript的执行。

JS的执行
V8 Javascript Engine —— 编译步骤,调用栈 和 堆 以及内存管理。
浏览器运行时 —— 并发模型,事件循环 以及 阻塞 和 非阻塞代码。

编译步骤
当浏览器加载JavaScript文件时,V8的解析器会将其转换为抽象语法树(AST)。Ignition使用此树生成字节码。V8在主线程中执行它,而优化编译器TurboFan在另一个线程中进行一些优化并生成优化的机器代码。

调用栈
调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

1.每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
2.正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被 调用,便会立即执行。
3.当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
4.分配的调用栈空间被占满时,会引发“堆栈溢出”错误。

JavaScript是具有单个调用堆栈的单线程编程语言。这意味着您的代码是同步执行的。每当函数运行时,它将完全在其他任何代码运行之前运行。
当V8调用您的JavaScript函数时,它必须将运行时数据存储在某个地方。调用堆栈是内存中由堆栈帧组成的位置。每个堆栈帧对应于对尚未返回的函数的调用。堆叠框架由以下组成:
局部变量
参数参数
退货地址


有时V8在编译时不知道对象变量将需要多少内存。此类数据的所有内存分配都发生在堆(内存的非结构化区域)中。退出分配内存的函数后,堆上的对象继续存在。
V8具有内置的垃圾收集器(GC)。垃圾回收是内存管理的一种形式。这就像一个收集器,它试图释放不再使用的对象占用的内存。换句话说,当变量失去所有引用时,GC会将其标记为“不可访问”并释放它。

浏览器运行时
因此,V8可以使用单个调用堆栈根据标准同步执行JavaScript。但是我们对此无能为力。我们需要渲染UI。我们需要处理用户与UI的交互。此外,我们需要在发出网络请求时处理用户交互。但是,当我们所有的代码都是同步的时,我们如何实现并发呢? 这有可能要归功于浏览器引擎。
浏览器引擎负责使用HTML和CSS渲染页面。在Chrome中,它称为闪烁。它是WebCore的一个分支,它是一个布局,渲染和文档对象模型(DOM)库。眨眼是C ++,并公开网络的API实现像DOM元素和事件XMLHttpRequest,fetch,setTimeout,setInterval等等,这是通过JavaScript访问。

并发
setTimeout函数执行完后,浏览器引擎会将setTimeout回调函数放入事件表中。这是一个将注册的回调映射到事件的数据结构,在我们的案例中,onTimeout函数将其映射到超时事件。
一旦计时器到期,在本例中,我们将延迟设为0 ms,则立即触发事件,并将onTimeout函数放入事件队列 (又名回调队列,消息队列或任务队列)。事件队列是一种数据结构,由将来要处理的回调函数(任务)组成。

阻塞与非阻塞
简而言之,所有JavaScript代码都被视为Blocking。 当V8忙于处理堆栈帧时,浏览器被卡住了。您的应用的用户界面已被阻止。用户将无法单击,导航或滚动。在V8完成工作之前,不会处理来自网络请求的响应。

在这里插入图片描述

在这里插入图片描述
V8
在这里插入图片描述
在这里插入图片描述

在v8v5.9中,Ignition和TurboFan第一次被普遍和专门用于JavaScript的执行。此外,从v5.9开始,自2010年以来一直为V8提供良好服务的完整codegen和chunkraft技术不再用于V8中的JavaScript执行,因为它们不再能够跟上新的JavaScript语言特性和这些特性所需的优化。我们计划很快将它们完全移除。这意味着V8将拥有一个更简单、更易于维护的体系结构。

解释器 Ignition:
最初的动机是减少移动设备上的内存消耗,在Ignition之前,由V8的Full-codegen基准编译器生成的代码通常占据Chrome中整个JavaScript堆的近三分之一。这为网络应用的实际数据留出了更少的空间。在具有有限内存的Android设备上为Chrome启用Ignition后,未优化的基准JavaScript代码所需的内存占用空间在基于ARM64的移动设备上减少了9倍。
编译器 TurboFan:
优化了当时JavaScript标准中的所有语言功能(ES5),而且还优化了ES2015 及以后计划的所有未来功能。它引入了分层的编译器设计,该设计使高级编译器优化和低级编译器优化之间清晰地分开,从而可以轻松添加新的语言功能,而无需特定于体系结构的代码。TurboFan添加了一个明确的指令选择编译阶段,从而可以为每个受支持的平台首先编写更少的特定于体系结构的代码。在这个新阶段中。特定于体系结构的代码只需编写一次,而很少需要更改。

v8 利用 Ignition 的字节码通过 TurboFan生成优化的机器码,而不必像Crankshaft那样从源代码重新编译。Ignition的字节码在V8中提供了更简洁,更不易出错的基准执行模型,简化了非优化机制,这是 v8 自适应优化的关键功能。最后,由于生成字节码比生成 Full-codegen的基线编译代码要快,因此激活Ignition通常可以缩短脚本启动时间,进而可以加载网页。

通过紧密结合Ignition和TurboFan的设计,整体架构将获得更多好处。例如,V8团队不使用手工编码的汇编编写Ignition的高性能字节码处理程序,而是使用TurboFan的中间表示来表达处理程序的功能,并让TurboFan为V8众多受支持的平台进行优化和最终代码生成。这确保了Ignition在V8所有受支持的芯片架构上均能良好运行,同时消除了维护9个独立平台端口的负担。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值