阅读 1029

从“快稳省安全”看Chromium——Chromium学习系列

前言

对于不同的角色,浏览器有着不同的意义:

普通用户通过它来浏览网页,在乎的是稳定,高效和安全;

前端开发同学关心的是兼容性和性能优化。我们可以通过学习Chromium的内部机制,更好的理解各种优化手段。

泛泛的介绍意义不大,我们从 快/稳/省/安全 这几个维度切入,看看浏览器是如何做到这几点的,这里以chromium为例,毕竟开源,且资料丰富。

加载快

Chromium拿到页面请求后,首先对需要请求的资源做分类,然后根据相关的安全策略,对资源做权限校验,接着对加载顺序按优先级排序,再根据顺序进行加载。

Chromium支持的资源分为如下14类:

类型

介绍

kMainResource

即主资源,html页面文件资源就属于该类型,包括location即导航输入地址得到的页面、使用frame/iframe嵌套的、通过超链接点击的页面以及表单提交这几种

kImage

图片资源

kCSSStyleSheet

css资源

kScript

脚本资源,例如js资源

kFont

字体资源,例如网页中常用的字体集.woff资源

kRaw

混合类型资源,最常见的ajax请求就属于这类资源

kSVGDocument

SVG可缩放矢量图形文件资源

kXSLStyleSheet

扩展样式表语言XSLT

kLinkPrefetch

HTML5页面的预读取资源(Link prefetch),例如dns-prefetch

kTextTrack

video的字幕资源,- 即<track>标签

kImportResource

HTML Imports,将一个HTML文件导入到其他HTML文档中,例如<link href="import/post.html" rel="import" />

kMedia

多媒体资源,video,audio都属于该类资源

kManifest

HTML5 应用程序缓存资源

kMock

预留的测试类型

资源的权限校验这块,放到安全那一节介绍。

Chromium对资源的优先级是这样定义的:

可以看到优先级总共分为五级:very-high、high、medium、low、very-low,其中页面、CSS、字体这三个的优先级是最高的,然后就是Script、Ajax这种,图片、音视频的默认优先级是比较低的,最低的是prefetch的资源。

资源按照优先级来下载,于体验有很大的帮助。

Prefetch(预加载)是个有意思的东西,有时候你可能需要让一些资源先加载准备好。

例如用户输入出错的时候在输入框右边显示一个X的图片,如果等要显示的时候再去加载就会有延时,这个时候可以用一个link标签:

<link rel="prefetch" href="image.png">复制代码

浏览器空闲的时候就会去加载。另外还可以预解析DNS:

<link rel="dns-prefetch" href="https://cdn.test.com"> 复制代码

预建立TCP连接:

<link rel="preconnect" href="https://cdn.chime.me">复制代码

资源缓存

资源的缓存是浏览器优化加载的重要手段。加载后的资源会存入资源缓存池,当chromium需要请求资源的时候,会优先从缓存池中查找,查找的key就是资源的URL。

资源缓存池使用LRU算法管理其中的资源,如果是强缓存s,又能命中cache的话,则不发送请求,如果是协商缓存,则仍向服务端发送请求。

多进程资源加载

Chromium采用的是多进程资源加载的机制:


Render进程(对应的是tab)没有权限下载资源,处于安全和效率的考量,Render进程的资源获取实际上是通过进程间通信(IPC)交给Browser进程(对应整个浏览器)来完成,Browser进程有获取本地或网络资源的权限。

这种架构的一个优点是,所有的资源请求都完全在I/O线程上处理,用户接口产生的活动与网络事件之间互不干扰。资源过滤器运行在Browser进程中的I/O线程中,截获资源请求消息,并将其转发给Browser进程中的ResourceDispatcherHost单例。

这个单例接口允许浏览器控制各个Render进程的网络访问权限,它还能实现高效和一致性的资源共享,例如:

1. socket池和连接限制:浏览器能够对每个profile、代理和{scheme, host, port}组所对应的已开启socket数量进行限制(分别为256、32和6个)。注意,按照这个规则,同一{host, port}最多可以进行6个HTTP和6个HTTPS连接。

2. socket重用:持久性的TCP连接会在请求处理之后在socket池中保留一段时间,以供连接重用,这样能够避免发起新的连接额外带来的DNS、TCP和SSL(如有需要)的启动开销。

3. socket后期绑定:当socket准备好分派应用程序请求时,请求才与基础的TCP连接绑定起来,这样一来可以获得更好的请求次序优化(例如:当socket在连接中时,更高优先级的请求到达),更大的流量(例如:在现有socket可用而新连接正在打开时,重用“刚使用过”的TCP连接)以及TCP预连接的通用机制和其他一些优化。

4. 一致的会话状态:所有Render进程的身份鉴证、cookies和缓存数据都是共享的。

5. 全局性资源和网络优化:浏览器可以从所有Render进程和未完成请求的全局做出决策。例如,对前景标签页发起的请求赋予网络优先级。

6. 预测性优化:通过观测所有的网络流量情况,Chromium能够构建和修正预测性模型提升性能。

就Render进程而言,它只是通过IPC发送资源请求消息,这个请求被打上对应Browser进程的唯一请求ID,剩下的部分都是由浏览器内核进程处理的。

启动快

影响页面渲染效率的因素不少,Chromium对此做了不少优化。

V8引擎的优化

2017年,V8做了重大升级,用上了Ignition(点火器) + TurboFan(增压涡轮)。

这样做的目的一是降低内存占用,二是减少启动时间,三是降低复杂度。


Composite

Composite是一种技术,Chromium会把页面分成多层,开启多个Compositor线程,分别对它们进行光栅化。当页面有滚动发生时,因为各层已经做了光栅化,所以要做的就是合成新的帧。

为了找出哪些元素属于哪一层,主线程会根据Layout Tree生成Layer Tree。如果你希望某个元素分层渲染,可以通过设置will-change这个样式属性,提醒浏览器这么做。


一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给compositor(合成器)线程。 compositor线程随后栅格化各层。 一个图层可能像页面的整个长度一样大,因此合成器线程将它们分成多个图块并将每个图块发送到光栅线程。 栅格线程栅格化每个图块并将它们存储在GPU内存中。


compositor线程为不同的图块分配不同的优先级,以便视口内的部分优先被光栅化。 图层还具有多个不同分辨率的拼接,以应付放大缩小等操作。

使用GPU

Chromium 利用 GPU 的最初的动力是 3D CSS,除了给 3D CSS 带来加速,GPU 同样可以用于提升普通的页面的渲染。这个理念在 Chromium 中得到了实现。首先 Chromium 将 GPU 加速运用到了 video canvas 等标签的渲染上,另外为高效合成 z 轴方向重叠元素,引入了图像合成的概念。

Chromium 利用 GPU 解决了这几个影响性能的问题:

1. 把部分光栅化的任务交给 GPU,降低绘制使用的时间(每帧从 100ms 降低到 4-5ms),为 JavaScript 的执行争取了更多的时间

2. 利用合成器,可以让一些 CSS 动画完全在 GPU 中绘制(Compositor Thread),不需要 CPU 的干预(Main Thread),即便被 JavaScript 阻塞也能保持动画流畅

3. 图层之间互不影响,减少了发生 reflow repaint 时所要遍历的元素数量

Chromium 在其多进程架构上引入了 GPU 进程。这个模型是可以伸缩的,在一些性能较低的平台上,GPU 进程可能会降为 GPU 线程。渲染进程对 GPU 的访问,会以指令的形式发送到 CommandBuffer(它是渲染进程和 GPU 进程共享的内存区域),然后通过 IPC 告知 GPU 进程。大体来说,比起 IPC 带来的损耗,这个架构带来的收益更加突出。因为绝大部分指令不需要返回值,这让渲染进程可以立即返回,并继续处理其他渲染任务。另外,将渲染进程隔离在不能直接访问 GPU 的安全沙盒中,这对 Chromium 提供 native 扩展的场景显得尤其重要。

运行流畅

多线程模型

页面运行不流畅的表现主要是janky(跳帧),作为前端同学,多少都知道一些关于如何避免跳帧,卡顿的优化做法,当然浏览器在这上面也做了不少改进。

为了解决页面卡顿的问题,Chromium使用基于异步通信的多线程模型。也就是说,一个线程请求另外一个线程执行一个任务的时候,不需要等待该任务完成就可以去做其它事情,以此避免卡顿。

事件的优化

一般我们屏幕的刷新速率为 60fps,但某些事件的触发会高于这个频率,出于优化的目的,Chromium 会合并连续的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延迟到下一帧渲染时候执行。

而keydown,keyup,mouseup,mousedown,touchstart和touchend 这些非连续事件则会立即被触发。



多进程模型

之前介绍多进程资源加载的时候,简单提到过Render进程和Browser进程,为了保证浏览器的稳定性,Chromium采用多进程的架构,将Render,Plugin和GPU进程与Browser进程分隔开来,这几个进程引发的Crash获取其他异常不会导致整个浏览器崩溃

Chromium的四类主要进程:

1. Browser进程

负责合成浏览器的UI,包括标题栏、地址栏、工具栏以及各个TAB的网页内容。一个Chromium实例只有一个Browser进程。

2. Render进程

负责解析和渲染网页的内容。一般来说,一个TAB就对应有一个Render进程。

我们也可以设置启动参数,让具有相同的域名的TAB都运行在同一个Render进程中。

无论是Browser进程,还是Render进程,当启用了硬件加速渲染时,它们都是通过GPU进程来渲染UI的。不过Render进程是将网页内容渲染在一个离屏窗口的,例如渲染在一个Frame Buffer Object上,而Browser进程是直接将UI渲染在Frame Buffer上,也就是屏幕上。

正因为如此,Render进程渲染好的网页UI要经过Browser进程合成之后,才能在屏幕上看到。

(题外话,说起离屏窗口,想到了之前项目中的一个做法:在一个bindows项目中,拖拽窗体时,将窗体内的元素扔到视口外,待拖拽停止后,再将它们放回来)

3. Plugin进程

用来运行第三方开发的Plugin,以便扩展浏览器的功能。Flash就是一个Plugin,它运行在独立的Plugin进程中。

为了避免创建过多的Plugin进程,同一个Plugin的不同实例都是运行在同一个Plugin进程中的。

也就是说,不管是在同一个TAB的网页创建的同类Plugin,还是在不同TAB的网页创建的同类Plugin,它们都是运行在同一个Plugin进程中。

4. GPU进程

GPU进程在前面介绍渲染时介绍过了,这里不再赘述。


在Chromium的官网文档中,貌似没有找到关于省电节能的内容。从网上查到的内容来看,Chromium似乎也是比较费电的,据说因为沙盒的限制,当Chromium播放视频时,只能使用GPU加速渲染,而不能加速解码。不过这块我并不了解,不做展开。

安全

当我们通过浏览器访问网页的时候,浏览器需要保护我们数据的安全。像cookie,账号/密码这类涉及个人隐私,交易支付的信息,不能被恶意网页获取。

同源策略

浏览器的安全模型中,域是十分重要的概念,域由协议+域名+端口构成。不同域之间的资源访问受到严格控制。页面的DOM用户数据XHR都无法跨域访问。

CSP

实际情况中,同源策略对于很多XSS(跨站脚本攻击)无能为力,因为不少资源是可以跨域加载的(image,JS)。浏览器提供了CSP来防止XSS,使用HTTP消息头来指定网页允许哪些域中的哪些资源可以被加载。

CSP通过HTTP头部由服务端制定,头部类型由于历史原因总共由三种,这三种仅仅是兼容性的差别,针对Chromium浏览器,我们仅需关注Content-Security-Policy头部。CSP头部的定义规则如下:

Content-Security-Policy: 名 值; 名 值; 名 值;


指令值的规范如下图:

HTTPS

这个大家都比较熟悉了,不做专门介绍。

沙箱模型

前面介绍过,Chromium是多进程架构,网页的渲染是在独立的Render进程中进行,这为实现沙箱模型提供了基础,将网页渲染放在一个权限受限的进程中进行。

沙箱模型依赖操作系统提供的技术,不同操作系统提供的安全技术不一样,所以沙箱模型在不同系统上的实现也是有差别的。

以Chromium在Android中的沙箱模型为例。据资料,Android的沙箱和Linux下是一样的。


Site Isolation

Site Isolation (网站隔离) 是 Chromium 为应对潜在的安全问题所实现的功能,以防止恶意网站获取其他网站的信息。

Site isolation 提供了同源策略之外的第二层的额外保护,将同源策略与进程的地址空间隔离结合起来把不同的网站隔离在不同的进程中,并且阻止一个进程获得其他网站的敏感信息。这样即使存在 spectre 类型的旁路攻击,可以获取进程内任意内存地址的数据,也不能获得其他网站的信息。

Site Isolation主要由两部分组成。进程模型的修改和跨域读取屏蔽 (CORB)。

site-per-process

目前 Chromium 默认的进程模型叫做 process-per-site-instance (还有其他的进程模型如 process-per-site 和 process-per-tab)[3]。这个进程模型基本上就是为每个页面创建一个进程,但是还是存在不同的网站用同一个进程的情况,如 iframes 和父页面同一个标签页里的页面跳转以及标签页过多的时候等。Site isolation 引入了一个新的策略叫做 site-per-process。这个策略更为严格,只要是不同的网站,不管你是在新的标签页打开,还是在同一个标签页跳转,还是嵌在 iframes 里,统统都要换一个新的进程。这里主要的工作量是把 iframes 给拿出来放到不同的进程里(所谓的 OOPIF, out of process iframe)。

使用同一个协议,同一个注册域名 (所谓的 eTLD+1) 的网址都属于同一个网站,这比同源策略里的 same origin 要宽泛一些,不同的子域名,不同的端口都算同一个网站。

CORB

CORB (Cross-Origin Read Blocking) 是一个屏蔽跨域资源加载的功能。

同源策略可以防止恶意网站获取其他网站的信息,但有一些例外如 <img>和<script>。类似<img src="https://example.com/secret.json">的跨站请求可以发起,只是返回的结果被过滤掉了,在解析图片时出错[5]。这时候跨域的资源其实已经传入到了这个进程里面,结合 spectre 类型的旁路攻击或者其他漏洞是可以拿到这些信息的。 CORB 的想法就是直接屏蔽掉跨域资源返回的结果,让地址空间里都没有返回的结果。目前只有HTML,XML 和 JSON 类型的资源会被 CORB 保护

参考资料

《Webkit技术内幕》

Chrome背后的故事(系列)

从Chrome源码看浏览器如何加载资源

理解WebKit和Chromium: WebKit资源加载机制

HTML5 Canvas,WebGL,CSS Shaders,GLSL的暧昧关系

活用 Shader,让你的页面更小,更炫,更快

浏览器页面资源加载过程与优化

缓存淘汰算法--LRU算法

Chrome中的高性能网络(二)

Chrome 渲染管道的性能改进

HTTP缓存——304与200 from cache

图解浏览器的基本工作原理

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

21天自制chromium之渲染篇(1)

chromium cc层的一处性能优化点

Inside look at modern web browser (part 4)

现代浏览器内部工作原理

Chromium网页渲染调度器(Scheduler)实现分析

Chromium网页渲染机制简要介绍和学习计划

Chromium网页加载过程简要介绍和学习计划

Multi-process Architecture

IE 安全系列:IE 浏览器的技术变迁(上)

IE安全系列:IE浏览器的技术变迁(下)

在新窗口中打开页面?小心有坑!

沙箱

Chromium多线程模型设计和实现分析

Chromium多进程架构简要介绍和学习计划

【OpenGL】17-帧缓冲与离屏渲染

Chrome 的 GPU 进程的背后

The whole web at maximum FPS: How WebRender gets rid of jank

Chrome Site Isolation 简介

渲染性能

XSS分析及预防

Why the New V8 is so Damn Fast

滚动优化(无限滚动加载、滚动元素内有大量dom,造成卡顿问题的优化方案)


关注下面的标签,发现更多相似文章
评论