Day08 X 浏览器架构演进史 & 渲染机制

「在未来,浏览器会变得越来越强,以後我们可以在浏览器做越来越多事。」

身为常与浏览器共舞的 Web 工程师,尤其是 Frontend Engineer,如果浏览器突然消失了,应该也等同於要失去饭碗了 ? 而上面这句话我想大家应该或多或少都有听过,不过你知道所谓变得越来越强是指什麽吗?浏览器又是为什麽会变得更强呢?今天,我想浅浅得说明浏览器的架构演进史,从过去,到现在,再看向未来。

身为 Web 工程师,浏览器的关系与我们密不可分,但除了学会使用它以外,如果能去理解背後的运作模式,我认为是百利而无一害的,除了学会根据背後运作模式去建构更好的 web 应用以外,也可以提早洞察到未来可能的发展,领先其他人一步去探索更多可能性。

也许有人会疑惑,浏览器的演进跟系列文主题效能优化有什麽关系咧?有,有关系。不过它是更底层的优化,想想平常做的优化还是建立在浏览器这个系统上,而浏览器本身的效能也是可以优化的。浏览器本身效能提升连带的影响是我们在 Web 开发多了更多可能性,身为 Web 工程师,我们应该对浏览器的效能进化感到兴奋,我有时候会想,10 年後,我们可以透过网页做什麽事情?这个问题,我不能断言,但我充满了期待。10 年前应该也没有多少人想到现在的浏览器也可以跑 AI Model 吧?所以带着期待的心情一起更认识浏览器吧!

今天的内容会分成两部分,第一部分会透过简易系统架构的角度去看浏览器的演进,process 程序(对岸用语为进程),thread 执行绪(对岸用语为线程)是两个必备的知识点,如果还不太了解两者概念的读者可以参考我之前关於 Process 与 thread 的文章。第二部分则会介绍浏览器的渲染引擎运作机制与渲染流程,是我们想提升网页效能一个非常重要的切入点,Let's Go!


第一部分:浏览器架构演进史

旧石器时代:Single Process 浏览器时期

首先这边假设各位读者都已经了解 Process 与 Thread 的基本概念。

其实在 2007 年以前,市面上的浏览器基本上都是 Single Process 单一程序的架构的。

这代表着浏览器的所有功能模组都是运行在同一个 Process 里的,所谓的功能模组指的是 JavaScript 执行环境、网路、浏览器扩充功能(插件)、渲染引擎…等,由上面的架构图也可以看出不同的功能模组可能运行在不同的 thread 中,然而这种架构衍伸出了几个明显的问题:

  • 不稳定性
  • 不流畅性
  • 安全性问题

不稳定性

简单来说就是,单一程序的浏览器中,其中一个功能模组如果出问题坏掉了,会导致整个浏览器的崩溃。早期的浏览器有很多功能并没有原生支援,例如影片播放、游戏引擎…等,需要透过插件 (Plugin) 来协助实现,但偏偏 Plugin 又很容易出问题,当插件运行崩坏时,也会导致浏览器的崩坏。JavaScript 执行环境也是一样的道理,如果程序码过於复杂或是效能炸裂,也会导致渲染引擎的崩坏,进而导致整个浏览器的崩坏。

不流畅性

从上面的 Single Process Browser 架构图可以看到,有一个叫 Page Thread 的执行绪,它的工作包山包海,从页面的渲染、页面的显示、JavaScript 的执行环境还有插件的运行都是它的职责。不过由於是单一执行绪,意味着同一时刻只有一个功能可以执行,所以如果遇到一段跑很慢或是无限循环的 JS 程序码,例如:

function stupidFunc() {
  while(true) {
   console.log('HI');
  }
};
stupidFunc();

在 Single Process 的浏览器架构里,上面这段无限循环的程序码会独占整个 thread 的资源,导致其他模组永远没有机会被执行到,因为浏览器所有的页面都是靠这个执行绪运行的,所以这样的状况会让整个浏览器失去反应或是变得卡顿。

另外相信大家也知道,记忆体的空间也是影响浏览器效能的重大因素,而 Single Process 架构的浏览器有一个明显的缺点就是往往不能完全地回收记忆体,导致使用时间越久记忆体占用就越来越高,导致页面慢慢变得不流畅与卡顿。

安全性问题

因为我本身对资安并不是那麽的了解,所以这边只能简单说明可能发生的状况。

以往 Single Process 架构的浏览器没有实作合理的安全环境(例如等等会提到的 sandbox),因此透过 Plugins 或是 Script 是有可能可以获取系统资源与权限的,而这个状况不需多说,自然会衍伸出许多安全性的问题,注入病毒、盗窃帐号密码都是可能发生的资安攻击。

以上就是三个 Single Process Browser 最大的缺陷,如果随便一个页面坏掉会使整个浏览器崩溃的话,确实蛮恐怖的,万一其中一个页面是你花了一个星期撰写,要给老板看的企业报告,你应该也会崩溃吧 ? 所幸现在的我们已经不需要受到这种浏览器架构的荼毒与威胁,让我们接着看看下个阶段浏览器的架构做出了什麽样的改变吧!

新石器时代:Multi Processes 浏览器时期

接下来的架构我都会以 Chrome 这个浏览器的架构来说明,并不是单纯因为它是我最喜欢的浏览器(虽然是事实没错 ?),更是因为它是第一个提出 Multi Processes 多程序架构的浏览器。Chrome 在 2008 年发表了 Multi Processes 的架构,当时的浏览器架构图大致上如下:

这个版本的架构与上一个 Single Process 的架构差别在於独立出了 Renderer Process 与 Plugins Process,也就是渲染程序与插件程序,不同 Processes 间需要通过 IPC 来沟通(也就是上图的虚线),Plugins Process 顾名思义就是专门运行浏览器插件、外挂的程序,Renderer Process 又被称作渲染引擎,主要负责页面的渲染,包含 Parse、Render、JS 的执行等工作,关於现今浏览器渲染引擎的运作机制,稍後会有更加详细的介绍。

接着来看看改成这样的架构後,是否有效地解决了 Single Process 浏览器的三大问题:

  • 不稳定性的问题:因为各个 Process 是互相隔离的,也就是说如果 Plugin 崩坏,或是页面失去响应了,只会影响到它们当前所运行的 Process,并不会像以前一样牵一发而动全身,导致整个浏览器的瘫痪,因此有效解决了不稳定性的问题。

  • 不流畅性的问题:这个架构下拆分出 Renderer Process 的特点是,浏览器中每一个 Tab 都会运行在独立的 Renderer Process 上。虽然遇到上面无限循环的 Script,ㄧ样会造成页面失去反应,不过现在会影响的就只有当前的页面(Tab)而已,其他的页面因为是运行在不同的 Process,因此仍能正常运作。再来是前面提到记忆体有可能无法完全回收或发生 memory leak 的问题,尽管现在页面失去反应了,当你关闭它时,整个 Renderer Process 也会被关闭,这个 Process 所占用的记忆体会被系统完整的收回,解决了过往 memory leak 的问题。因此这样看下来,不流畅性的问题也得到了大大的改善。

  • 不安全性的问题:在 Multi Processes 架构下的浏览器,不仅独立出了不同的程序,还引用了 「Sandbox 沙盒」的机制,使 Plugin Process 与 Renderer Process 运行在沙盒中。(可以把 Sandbox 想像成一个安全的隔离环境,在里面运行的程序无法获取外部的数据,放到浏览器中,就是指无法获得系统的资源,想当然,连读取都禁止了,写入当然也是禁止的),即使有恶意的程序码,也只会运行在 Sandbox 中,无法突破 Sandbox 影响外部浏览器的系统,这也就解决了之前提到可能会发生的一些资安问题。

现代:更加丰富的 Multi Processes 浏览器架构

(可能有历史很好的读者会疑惑,怎麽从新石器时代马上就跳到现代来了 ?,这点就放过我不要计较了吧 ?)

大家都知道 Chrome 是 Google 开发的,进化的速度也是非常快的,现在的 Chrome 浏览器仍然以多程序架构为基础,不过独立出了更多的 Processes,大致上如以下的架构图:

从上图可以看出,原本运行於 Browser Process 中的网路资源操作与 GPU 操作变成 Network Process(主要负责网路资源载入) 与 GPU Process(负责一些页面的绘制与运算) 被独立了出来,而独立出 Process 的好处也在稍早提过了,除了解决不稳定性、不安全性与不流畅性以外,也可以拥抱 process parallel 运行带来的性能提升。

那麽方便的话,我把所有操作都独立出一个 Process 不就好了?

这个思考方向并没有错,不过想拥有收获总是得付出一些成本或代价,独立出更多 Processes 的缺点主要有:

  • 更高的记忆体占用
  • 系统架构会变得更加复杂(要考虑不同 process 间的沟通)
  • 因此这其实是个非常复杂的问题,也是 Chrome 团队一直在优化的方向。

未来世界:SOA 服务导向架构浏览器

面对上述高记忆体占用与架构复杂的问题,Chrome 团队非常努力想找到一个弹性的解决方案,在 2016 年 chrome 提出了以 SOA (Services Oriented Architecture) 服务导向架构为基础的新架构:

也就是希望各个在 browser 中运行的 program 可以以服务 (service) 的角度被拆分或聚合,并运行在独立的 Process 中,Processes 间透过 IPC 来沟通,让系统架构实现高内聚、低耦合、易扩展与易维护的特性。

而关於 SOA,相信很多读者会联想到 Microservices,如果想了解两者区别的读者可以参考这篇文章

另外 Chrome 还提供一个我认为非常厉害又弹性的架构,我们都知道设备的性能差异是很大的,如果在低阶的设备上,例如老旧的手机,这样的浏览器架构似乎不是低阶设备承受得起的。在遇到性能较高的设备时,Chrome 会采用上面所说拆成多个 Processes 的架构去增强稳定性与效能,但是如果是在较低阶老旧的设备上运行时,Chrome 则会自动采用多个服务合并成单一 Process 的方式来节省记忆体耗费,来达到更弹性的架构。

General idea is that when Chrome is running on powerful hardware, it may split each service into different processes giving more stability, but if it is on a resource-constraint device, Chrome consolidates services into one process saving memory footprint. — developer.google.com

其实 Chrome 现在就已经在朝着这个方向前进了,只不过这必定是一个缓慢的过程,因此我将它放在「未来」这个时间线里。不过值得一提的是,Chrome 的更新是渐进式的,也就是说未来服务会慢慢的改进与更新,我们将会慢慢享受到更多更新的服务,未来不管是 AR/VR、游戏引擎,甚至是 AI,都可以在浏览器身上看到无限可能,身为 Web 开发者,我想这是一件会让我们都感到期待与热血沸腾的一件事。


第二部分:现今浏览器渲染引擎的运作机制

原本这篇文章应该在上面简单介绍完浏览器架构演进後就该告一段落了,不过刚好在这篇文章中一直提到了 Renderer Process,它也就是你常常会听到的「渲染引擎」,身为 Web 工程师,又甚至像我一样是更偏向前端开发的工程师,渲染引擎想必是你最常听到,也最在乎的一个程序,因为它负责处理页面的渲染流程,负责将 HTML、CSS、JavaScript 三剑客变成我们看到的页面,然而这中间发生的过程你是否都了解了呢?如果你还不是很了解也没关系,我想藉着文章的最後一个段落(虽然应该会是最长的一个段落),简单复习一下渲染引擎的运作机制,让各位开发者们可以更了解网页究竟是怎麽被显示出来的。

(底下ㄧ样会以 Chrome 浏览器作为示范)

每个 Tab 都会产生一个独立的 Renderer Process

这点在看完上面的架构演进史後,读者应该都能了解了,这也就是为什麽其中一个 Tab 的网页挂掉之後,你仍然可以继续正常使用其他 Tab 的网页,因为不同的 Renderer Process 是不会互相影响的。读者可以点击 Chrome 浏览器的右上角,点击「更多工具」->「工作管理员」

会出现类似作业系统工作管理员的介面

在了解浏览器架构演进史後,看到浏览器运行着这麽多 Process 应该就不会被吓到了,除了 Browser Process 以外, GPU Process、Network Process 等不同程序都可以在工作管理员看到,再来就是各个不同的 Tab 也会运行在独立的 Renderer Process 里。

Per-frame Renderer Processes — Site Isolation

这是 Chrome 在 2018 年左右引进的新特色,Same Origin Policy 同源政策是 web 里一个很普遍的安全模型,理论上不同源的网站在未经授权下是要不能存取到彼此的资源的,不然会产生许多安全性问题。而要做到把两个不同来源的网站彻底分开,独立 Process 成为最有效率也最根本的一个方式,因此在 Chrome 中,实现了每个 Tab 都独立一个程序的机制,甚至在网页中嵌入不同来源的 iframe,该 iframe 也会运行在不同的 Renderer Process 上:

不过读者要知道采用这种方式不仅仅是独立出不同 Renderer Processes 这麽单纯而已,它也彻底改变了 iframe 与网页间的沟通方式,对於 Chrome 团队来说绝对是一个很大的里程碑。

不过眼尖的读者可能会发现,有些页面显示为「子页框」,并且没有独立的 Process ID,这是为什麽呢?

Process Per Site Instance

虽然预设状况下,每个 Tab 都会是独立一个 Renderer Process,不过在某些「Same Site」的状况下,Chrome 预设会将同源的网页运行在同一个 Process 中。这里的 Same Site 指的是 Protocol ㄧ样、root domain 一样就符合了(跟一般 Same Site Cookie 或同源政策的标准都不太一样,读者别搞混罗,请把它们当作完全不同的观念!),也就是说

https://kylemo.com
https://www.kylemo.com
https://www.kylemo.com:3000

都会被视为 Same Site,另外还有一个条件是「必须从一个页面打开另一个 Same Site 页面」 ,例如透过 tag 或是 window.open 等方法,浏览器就会将新开启的 Same Site 页面与原本的页面运行在同一个 Process 中。其实仔细想想这样的特性是合理的,毕竟有些 Same Site 的网页,是有共享 JavaScript 执行环境的需求的,另外节省记忆体也是这个特性的优势之一。

渲染流程

Renderer Process 主要负责的就是页面的渲染流程,这边还是简单说明下,在浏览器输入 URL 并按下 Enter 後,搜寻列会先对输入做解析,判断使用者输入的是 URL 还是搜寻关键字,并透过 Network Thread 或是 Network Process 去做资源请求,并根据回传的 Content-Type 来决定下一步要交给谁做,如果是回传的是 HTML,就会准备交由 Renderer Process 进行渲染流程。


读者可以透过 CURL 来看看 Response Header 中的 content-type

基本上如果你上网查询浏览器渲染流程,应该都会看到跟下面这张差不多的图片:


图片来源

大致上网页的渲染流程为:

  • 读取 HTML 後生成 DOM Tree
  • 读取 HTML 中的 CSS Link Tag 生成 CSSOM Tree
  • DOM Tree 与 CSSOM Tree 共同生成 Render Tree
  • 根据 Render Tree 生成 Layout Tree,负责各元素大小与位置的计算
  • 最後 Paint 画面

但是,其实浏览器在 Layout 之前与 Paint 之後的过程还做了一些事。

而这些事是一般在网路查询渲染流程时不太会被介绍到的,往往被开发者所忽略的部分,虽然主要是因为浏览器帮我们做好了,不知道这些事也不会影响到开发者的开发流程,不过如果对整个渲染流程有更完整的理解,一定是利大於弊的。

Layer 分层

为了方便实现一些浏览器上的复杂效果例如页面滚动或是三维空间的排序,浏览器会根据 Layout Tree 产生 Layer Tree


图片来源

如果使用过 Adobe Photoshop 的读者应该都知道,我们最後产出的图,实际上就是由许多图层叠加在一起的,而浏览器上的页面其实也是ㄧ样的,这边读者还不需要去理解浏览器是怎麽安排哪些节点应该要变成一个独立的图层的,这是一个非常复杂的技术,各位读者目前只需要知道「浏览器上的页面也是由许多图层叠加在一起的」就足够了,如果真的有兴趣了解背後机制的读者可以再自行研究。


图片来源

在将页面分层以後,就要对每个图层进行绘制了,不过我想大部分的人都会以为真正的绘制行为就是在 Renderer Process 中的 Main Thread 执行的,不过其实这个阶段做的只是「产生绘制指令」而已,所谓的绘制指令就是告诉浏览器在哪个座标要绘制线或是绘制几何图形等简单指令的集合,後续的操操作则是会在生成绘制指令後转交给 Renderer Process 中的另一个执行绪 — Compositor Thread 来接棒。

Compositing

现在浏览器已经获得了渲染页面所需要的资讯了,例如 DOM Tree 的结构、每个节点的 Style、节点在页面中的几何位置,还有刚刚提到的各个图层的绘制指令与叠加顺序…等,因此已经准备好可以绘制到页面上了。

首先读者得先了解一个专有名词 — rasterize 栅格化,也就是把上述页面资讯转变成 pixels 显示在萤幕上。如果不考虑分层的话,最符合常理的状况就是一次 rasterize 出现在 viewport 里的部分,如果使用者滑动了页面,再去 rasterize 新出现在 viewport 的部分。在旧的 Chrome 架构中的确是采用这种方式,不过随着浏览器进步,现在采用的是更复杂,但是整体效能更好的流程,也就是标题的 compositing,中文可以称作合成。

先前已经提到浏览器根据 Layout Tree 生成了 Layer Tree,compositing 的概念就是各个 Layer 分别做 rasterize,并在 Compositor Thread 把各个经过栅格化的图层组合起来,此时如果有页面滚动事件产生,因为每个 Layer 已经经过栅格化,所以要做的事就是合成一个新的 frame 就好。

不过每个 Layer 的大小不ㄧ样,有些 layer 可能几乎包含整个页面的大小,会导致效能受到影响,因此 Compositor Thread 会再将一个 Layer 切分成更小的单位 — tile(栅格化的最小单位),并把这些 tiles 送到真正负责栅格化的 raster threads,raster threads 将 tiles rasterize 後会存放到浏览器的 GPU 储存空间里。Tiles 被栅格化之後,Compositor Thread 会汇集被称作 draw quads 的资讯来产生 Compositor Frame。

Compositor Frame 接着会藉由 IPC 被送到 Browser Process,最後送到 GPU 去显示到画面上。


图片来源

所以看到这我们应该可以把原本的渲染流程图修改成下面这样:


Reflow & Repaint & Compositing

最後来谈谈页面更新造成的 Reflow 回流、Repaint 重绘与 Compositing 合成,这是三个与页面效能高度相关的概念。

Reflow 回流

指的是浏览器为了重新渲染部分或全部的 document 而重新计算 Render Tree 中元素的物理属性,如位置、大小的过程。

触发条件为改变一些元素的几何样式,例如 height、width、margin 或是排列的方式等等。

Repaint 重绘

将计算结果转为实际的像素,画到画面上。
如果只改动元素的颜色、背景图等不需要重新计算页面元素 layout 的样式,就只会从 Repaint 开始触发,跳过 Reflow 的步骤,最後再到合成阶段。

Composition 合成

也就是刚刚提到的合成。


(页面更新简易流程)

这边需要了解的是,如果触发了渲染流程的某个阶段,那麽其之後的阶段就也会被触发。透过文章前面的渲染流程内容,我们大致可以推断出,Reflow 跟 Repaint 分别会触发渲染流程中的哪些步骤


(画得很丑请见谅QQ)

了解这些以後,应该可以发现,不同的改变样式的方式,是会触发不同渲染流程的,因此也是效能优化的一个方向:

1. 改变一些 Layout 相关属性

例如 width、height、position 的 left 或 top 等,浏览器会需要重新计算页面元素的 layout,因此渲染流程没有步骤可以被省略,Reflow、Repaint、Compositing 都会被触发。

2. 只改变一些「paint only」的属性

例如背景图片、字体颜色等不需要重新计算 layout 的属性,Reflow 就不会被触发,Layout 会被跳过,只会触发 Repaint 之後的流程。

3. Compositing Only

也就是更改一些不需要 Reflow 与不需要 Repaint 的属性,例如 CSS 的

transform: translate(xxx, yyy);

这种改变方式是效率最好的,除了因为它不需要经过 Reflow 与 Repaint ,只需要做合成以外,还有一个重点是「合成的运作不是在 Main Thread 进行的,而是在 Compositor Thread 与 Raster Thread,因此不会占用 Main Thread 资源」,这也是为什麽要做 CSS 动画会建议使用 transform 的原因。


(很丑,再次道歉QQ)

如何避免多余的 Reflow 与 Repaint?

  • 避免用多个 statement 修改 style,建议使用新增或移除 class 的方式。
  • 先一次读取完,再一起修改

从上图范例可以看到 read 一次就马上 write 一次的写法会造成 6 次 Reflow,然而

这种一次 read 完全部再 write 全部的写法却只会造成 1 次 Reflow,在复杂的应用中也许两种方式会造成页面效能产生很大的差异,而背後原因则跟浏览器更深入的运作机制有关,这边就不多加探讨。

其实优化 Reflow 与 Repaint 的方式还有许多,有兴趣的读者可以参考这里,也可以在这里看到各种 CSS 属性改变会触发的渲染阶段。

Reflow、Repaint 与Compositing 的简单介绍就到这里。其实上面说的这些也只是基础中的基础,可见写 CSS 也是水很深的一门学问啊,如果想知道怎麽好好掌握 Reflow、Repaint、Compositing,来避免写出消耗渲染性能的程序码,建议读者参考这篇文章

本日小结

李开复曾说过:「浏览器就是未来的作业系统。」这句话曾引起了广泛的讨论,持正反意见的人都有,虽然现在看下来,要取代作业系统有一定的难度,不过可以肯定的是,浏览器只会越变越强,能在浏览器上做的事只会越来越多,并且执行效能只会愈来越好。

身为一个需要与浏览器共舞 Web 开发者,除了要知道浏览器越变越厉害以外,也该知道它进步的原因,背後的架构是怎麽演进的,还有它在短时间的未来可能会成长成什麽样子,才能好好的拥抱这波技术大跃进,发展更多的可能性。希望今天的内容可以使读者对於浏览器有更深的理解,也激发出对它的兴趣,从更底层的角度寻找效能优化的可能性,我们明天见罗!

References

https://developers.google.com/web/updates/2018/09/inside-browser-part1
https://blog.poetries.top/browser-working-principle/
https://gist.github.com/faressoft/36cdd64faae21ed22948b458e6bf04d5


<<:  推论统计-z检定、t检定是什麽?

>>:  Day08 - 寻找看板

[Day21] 回呼函式 Callback Function

先来看看 MDN 的定义。 回呼函式(callback function)是指能藉由 argumen...

不只懂 Vue 语法:参赛初衷与文章方向

参赛初衷 今年的参赛题目是「不只懂语法:Vue.js 观念篇」。去年的这时候刚刚学 JavaScri...

unRaid安装+基本设置

安装前注意事项 unRaid采用随身碟开机(系统跟引导区亦装在随身碟里) 所以须注意随身碟的挑选,千...

被供应商和客户G爆时来一帖「资讯安全oo声明书」吧

资讯安全管理制度(ISMS)相关文件属於企业或机构内部资安治理运行标准与纪录,机密等级(控管标准)大...

Day 8. 版控很重要!

在遥远的远古时期,专案的程序码都是丢到网路芳邻上时,大家都是用资料夹在做备份跟还原,如果多人开发同个...