Chrome浏览器渲染原理笔记

以前没怎么系统性的深入学习这方面,今天看了篇文章后还是觉得要好好整理归纳下的。顺便尝试能不能养成写笔记的习惯。

目录

浏览器的架构

Chrome浏览器的多进程架构

多进程架构的好处

多进程架构优化

Chrome浏览器的进程模式

导航过程都发生了什么

网页加载过程

第一步:地址栏处理输入

第二步:开始导航

第三步:读取响应

第四步:查找渲染进程

第五步:提交导航

第六步:初始化加载完成

个人总结

网页渲染原理

构建DOM

资源子加载

JavaScript的下载与执行

样式计算 - Style calculation

布局 - Layout

绘制 - Paint

合成 - Compositing

浏览器对事件的处理

渲染进程中合成器线程接收事件

查找事件的目标对象(event target)

浏览器对事件的优化

总结(无情的copy机)

参考文献

1、你不知道的浏览器渲染原理

2、Google Safe Browsing API的实施

3、渲染流水线中的光栅化(一)


浏览器的架构

 在讲浏览器架构之前,先理解两个概念,进程线程

进程(process)是程序的一次执行过程,线程(thread)是CPU调度和分派的基本单位,也是跑在进程里面的,一个进程里面可能有一个或者多个线程,而一个线程只能隶属于一个进程。

因为浏览器是一个应用程序,而应用程序的一次执行,可以理解为计算机启动了一个进程。进程启动后CPU会给该进程分配相应的内存空间,当我们的进程得到了内存之后,就可以使用线程进行资源调度,进而完成我们应用程序的功能。同时应用程序可以采取多进程的方式来工作,因为进程和进程之间是互相独立的它们互不影响,也就是说当其中一个进程挂掉了之后,是不会影响到其他进程的执行,只需要重启挂掉的进程就可以恢复运行。

在应用程序中,为了满足功能需要,启动的进程也会创建另外的新的进程来处理任务,这些新进程也拥有全新的独立内存空间(个人推测内存泄露应该就和这个有关)。

Chrome浏览器的多进程架构

在Chrome中,主要的进程有4个:

  • 浏览器进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。

  • 渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。

  • 插件进程 (Plugin Process):负责控制网页使用到的插件

  • GPU进程 (GPU Process):负责处理整个应用程序的GPU任务

进程关系:
1、在浏览器的地址栏里输入URL,浏览器进程 (Browser Process)会向该URL发送请求,获取这个URL的HTML内容并交给渲染进程 (Renderer Process);

2、渲染进程 (Renderer Process)解析HTML内容时,解析若遇到需要请求网络的资源将会返回给浏览器进程 (Browser Process)进行加载,同时通知Browser Process需要Plugin Process加载插件资源时要执行插件代码。解析完成后,Renderer Process计算得到图像帧,并将这些图像帧交给GPU Process。

3、GPU Process将其转化为图像显示屏幕。

多进程架构的好处

一、更高的容错性。当今WEB应用中,HTML,CSS和JavaScript日益复杂,这些跑在渲染引擎的代码,频繁的出现BUG影响甚至导致渲染引擎崩溃,多进程架构使得每一个渲染引擎代码在各自的进程中相互不受影响,哪怕其中一个页面崩溃挂掉之后,其它页面还可以正常运行不爱影响。

二、更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠。(vue文档中也对v-html描述有安全性问题,容易导致 XSS 攻击

三,更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺CPU资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。

多进程架构优化

渲染进程 (Renderer Process)的作用是负责一个Tab内的显示相关的工作,这就意味着一个Tab就会有一个渲染进程,这些进程之间的内存无法进行共享,而不同的进程内存需要包含相同的内容。这里有点绕,就是说进程都是占独立内存,不过它们跑任务时会用到共用或常的内容(比如一些行为)。

Chrome浏览器的进程模式

为了节省内存,Chrome提供了四种进程模式(Process Models),不同的进程模式会对 tab 进程做不同的处理。

  • Process-per-site-instance (default) - 同一个 site-instance 使用一个进程

  • Process-per-site - 同一个站点(site)使用一个进程

  • Process-per-tab - 每个 tab 使用一个进程,所以每新建一个tab就会新建一个进程

  • Single process - 所有 tab 共用一个进程

这里需要给出 site 和 site-instance 的定义

  • site 指的是相同的 registered domain name(如:google.com ,bbc.co.uk)和scheme (如:https://)。比如a.baidu.com和b.baidu.com就可以理解为同一个 site(注意这里要和 Same-origin policy 区分开来,同源策略还涉及到子域名和端口)。

  • site-instance 指的是一组 connected pages from the same site,这里 connected 的定义是 can obtain references to each other in script code 怎么理解这段话呢。满足下面两中情况并且打开的新页面和旧页面属于上面定义的同一个 site,就属于同一个 site-instance

    • 用户通过<a target="_blank">这种方式点击打开的新页面

    • JS代码打开的新页面(比如 window.open)

导航过程都发生了什么

网页加载过程

上文有提到,tab以外的大部分工作由浏览器进程Browser Process负责,针对工作的不同,Browser Process 划分出不同的工作线程:

  • UI thread:控制浏览器上的按钮及输入框;

  • network thread:处理网络请求,从网上获取数据;

  • storage thread:控制文件等的访问;

第一步:地址栏处理输入

当我们在浏览器的地址栏输入内容按下回车时,UI thread会判断输入的内容是搜索关键词(search query)还是URL,如果是关键词就跳转至默认搜索引擎对应都搜索URL,如果是URL就开始请求URL。(这段其实不想抄的,感觉上过网的多多少少都知道)

第二步:开始导航

索引开始,UI thread把对应的地址交给网络线程(Network thread),此时UI thread线程使该Tab前的图标展示为加载中状态,然后网络进程(Network thread)进行一系列操作(比如:DNS寻址,建立 TLS连接等)进行资源请求。

此时将会收到HTTP状态码,若为3xx若需要重写向的url跳转,则跳转并请求

1**信息,服务器收到请求,需要请求者继续执行操作
2**成功,操作被成功接收并处理
3**重定向,需要进一步的操作以完成请求
4**客户端错误,请求包含语法错误或无法完成请求
5**服务器错误,服务器在处理请求的过程中发生了错误

第三步:读取响应

network thread接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。

与此同时,浏览器会进行 Safe Browsing(安全浏览) 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。对于Google来说,他有自己的开源API, 通过把目标网站POST到Google的服务区上,来查看返回值,已确定网站是否安全。

除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

第四步:查找渲染进程

各种检查完毕以后,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。

浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。

第五步:提交导航

到了这一步,数据和渲染进程都准备好了,Browser Process 会向 Renderer Process 发送IPC消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送IPC消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。

这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。

第六步:初始化加载完成

当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的iframe都触发了onload事件),会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标。

个人总结

根据上文总结如下:

1、在浏览器的地址栏里输入内容,浏览器进程 (Browser Process)下的UI thread会把对应的URL交给Browser Process下的网络进程(Netword thread)发送请求并读取响应,解析HTTP响应报文并根据其响应头里的Content-Type字段分析的媒体类型

  • 若媒体类型是下载文件,会把相关数据传输给浏览器的下载管理器;

  • 若媒体类型是一个html文件则浏览器会进行安全浏览(Safe Browsing)检查,Netword thread还会做CORB检查,各种检查后都没问题network thread会通知UI thread数据已经准备好,UI thread会查找到渲染进程 (Renderer Process)进行网页渲染。

2、渲染进程 (Renderer Process)解析HTML内容时,解析若遇到需要请求网络的资源将会返回给浏览器进程 (Browser Process)进行加载,同时通知Browser Process需要Plugin Process加载插件资源时要执行插件代码。解析完成后,会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标

网页渲染原理

导航过程完成之后,浏览器进程把数据交给了渲染进程,渲染进程负责tab内的所有事情,核心目的就是将HTML/CSS/JS代码,转化为用户可进行交互的web页面。那么渲染进程是如何工作的呢?

渲染进程中,包含线程分别是:

  • 一个主线程(main thread)

  • 多个工作线程(work thread)

  • 一个合成器线程(compositor thread)

  • 多个光栅化线程(raster thread)

构建DOM

当渲染进程接受到导航的确认信息后,开始接受来自浏览器进程的数据,这个时候,主线程会解析数据转化为DOM(Document Object Model)对象。

资源子加载

在构建DOM的过程中,会解析到图片、CSS、JavaScript脚本等资源。这些资源是需要从网络或者缓存中获取的,主线程在构建DOM过程中如果遇到了这些资源,逐一发起请求去获取,而为了提升效率,浏览器也会运行预加载扫描(preload scanner)程序,如果HTML中存在imglink等标签,预加载扫描程序会把这些请求传递给Browser Process的network thread进行资源下载。

JavaScript的下载与执行

构建DOM过程中,如果遇到<script>标签,渲染引擎会停止对HTML的解析,而去加载执行JS代码,原因在于JS代码可能会改变DOM的结构(比如执行document.write()等API)

不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script> 标签上添加了 async 或 defer 等属性(虽然笔者是没用过,没用MVVM框架之前,还是JQ的年代时,笔者是把共用的js文件放在head标签里按优先级排序,再把该页面会用到的js文件放在body后面),浏览器会异步的加载和执行JS代码,而不会阻塞渲染。

样式计算 - Style calculation

DOM树只是我们页面的结构,我们要知道页面长什么样子,我们还需要知道DOM的每一个节点的样式。主线程在解析页面时,遇到<style>标签或者<link>标签的CSS资源,会加载CSS代码,根据CSS代码确定每个DOM节点的计算样式(computed style)。

计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,即使你的页面没有设置任何自定义的样式,浏览器也会提供其默认样式(每个浏览器的默认样式都不一致,要reset下)。

布局 - Layout

DOM树和计算样式完成后,我们还需要知道每一个节点在页面上的位置,布局(Layout)其实就是找到所有元素的几何关系的过程。

主线程会遍历DOM 及相关元素的计算样式,构建出包含每个元素的页面坐标信息及盒子模型大小的布局树(Render Tree),遍历过程中会跳过隐藏的元素(display: none),另外伪元素虽然在DOM上不可见,但是在布局树上是可见的。

绘制 - Paint

布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录(paint records)。绘画记录可以看做是记录各元素绘制先后顺序的笔记。

合成 - Compositing

这一块真要理的话,笔者自己都有点乱,水有点深,特别是光栅化内容(有兴趣的话去看下参考文献1和3)。笔者是没有兴趣再深入了,简单整理如下:

把结构、元素的样式、元素的几何关系、绘画顺序这些信息转化为显示器中的像素时,这个转化的过程叫做光栅化(rasterizing)。

合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。

 

Chromium 使用的是异步分块光栅化的策略,除了一些特殊图层外(比如 Canvas,Video):

  1. 图层会按一定的规则切割成同样大小的分块,这些分块会覆盖整个图层;
  2. 在 Viewport 范围内或者附近的分块会分配大小跟分块相同的像素缓冲区,当 Viewport 发生变化时,会重新分配或者回收这些像素缓冲区;
  3. 光栅化是以分块为单位进行,每个光栅化任务执行对应图层的对应分块区域内的绘图指令,结果写入该分块的像素缓冲区;
  4. 光栅化和合成不在同一个线程执行,并且不是同步的,如果合成过程中某个分块没有完成光栅化,那它就会保留空白或者绘制一个棋盘格的图形(Checkerboard);

对于异步光栅化来说,为图层分配额外的像素缓冲区是必须的,而使用分块的方式比起分配一个完整大小的像素缓冲区有很多优势:

  1. 为超大图层分配一个完整大小的像素缓冲区可能超过硬件支持的范围;
  2. 超大图层只有部分可见,为不可见的部分分配像素缓冲区会导致内存的浪费;
  3. 如果一个图层只有部分区域发生变化,只需要重新光栅化关联的分块;
  4. 尺寸大小固定的小分块,可以通过一个资源池(Resource Pool)统一管理这些像素缓冲区,方便回收和重分配;

浏览器对事件的处理

当页面渲染完毕以后,TAB内已经显示出了可交互的WEB页面,用户可以进行移动鼠标、点击页面等操作了,而当这些事件发生时候,浏览器是如何处理这些事件的呢?

以点击事件(click event)为例,让鼠标点击页面时候,首先接受到事件信息的是Browser Process,随后便把事件的信息传递给了渲染进程,渲染进程会找到根据事件发生的坐标,找到目标对象(target),并且运行这个目标对象的点击事件绑定的监听函数(listener)。

渲染进程中合成器线程接收事件

由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为非快速滚动区域(non-fast scrollable region),如果事件发生在这些存在标注的区域,合成器线程会把事件信息发送给主线程,等待主线程进行事件处理,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。

而对于非快速滚动区域的标记,需要注意到全局事件的绑定,比如事件委托将目标元素的事件交给根元素body进行处理,示例如下:

document.body.addEventListener('touchstart', event => {
  if (event.target === area) {
    event.preventDefault()
  }
})

这一段代码给body元素绑定了事件监听器,那么整个页面都被编辑为一个非快速滚动区域,哪怕你的页面的某些区域没有绑定任何事件,然而每次用户触发事件时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。

在这种情况下需要在事件监听时传递passtive参数为 true,passtive会告诉浏览器你既要绑定事件,又要让组合器线程直接跳过主线程的事件处理直接合成创建组合帧。示例如下:

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

查找事件的目标对象(event target)

当合成器线程接收到事件信息,判定到事件发生不在非快速滚动区域后,合成器线程会向主线程发送这个时间信息,主线程获取到事件信息的第一件事就是通过命中测试(hit test)去找到事件的目标对象。具体的命中测试流程是遍历在绘制阶段生成的绘画记录(paint records)来找到包含了事件发生坐标上的元素对象。

浏览器对事件的优化

一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗。

总结(无情的copy机)

浏览器的多进程架构,根据不同的功能划分了不同的进程,进程内不同的使命划分了不同的线程,当用户开始浏览网页时候,浏览器进程进行处理输入、开始导航请求数据、请求响应数据,查找新建渲染进程,提交导航,之后渲染又进行了解析HTML构建DOM、构建过程加载子资源、下载并执行JS代码、样式计算、布局、绘制、合成,一步一步的构建出一个可交互的WEB页面,之后浏览器进程又接受页面的交互事件信息,并将其交给渲染进程,渲染进程内主进程进行命中测试,查找目标元素并执行绑定的事件,完成页面的交互。

参考文献

1、你不知道的浏览器渲染原理

https://mp.weixin.qq.com/s/QVfBgLXHKLQS-RkRi-cf4w

2、Google Safe Browsing API的实施

https://blog.csdn.net/st_andrews/article/details/78673453

3、渲染流水线中的光栅化(一)

https://zhuanlan.zhihu.com/p/78758247

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值