从浏览器理解前端知识(二)

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

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

当浏览器为用户呈现一个页面时,这些进程与线程之间是如何通信的?
以一个常见的例子作为起点:输入一个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可能在此时持续地加载资源并渲染新的视图。

渲染进程处理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的执行。

从浏览器理解前端知识(二)