现代浏览器内部工作原理
汇总图



计算机的核心CPU和GPU

CPU:Central Processing Unit(中央处理器),中心处理器是计算机的大脑,每个CPU核心会逐一执行不同任务现在很多计算机都是多芯片,多内核的。



GPU:Graphics Processing Unit(图形处理器),GPU擅长处理跨内核的简单任务,是为了解决图形而开发的。在图形环境中,“使用GPU支持”和“使用CPU”都与快速渲染与顺滑渲染有关。



许多带扳手的GPU内核说明它们只能执行处理有限任务

当启动电脑时候,是CPU和GPU为应用供能。通常情况下,应用是通过操作系统提供的机制在CPU和GPU上运行。



三层计算机系结构:底部是机器硬件,中间是操作系统,顶层是应用程序。
在进程和线程上执行程序 进程(process)和线程(thread)



进程作为边界框,线程为里面抽象的鱼在游动

进程可以被描述为一个应用的执行程序,线程存在于进程并执行任意部分。

启动应用时,会创建一个或者多个进程来帮助应用工作,操作系统为进程提供了一块可以使用的“内存”,应用的所有状态都保存在该私有内存空间中,关闭应用,进程会关闭,操作系统释放内存。



进程可以请求操作系统的另一个进程来执行不同的任务。此时,会分配不同的内存给新进程,进程之间可以通过(IPC:Inter Process Communication)进行通讯。应用的某个工作进程失去响应,该进程就可以在不停止应用程序其他进程的情况下,重启。



独立进程通过IPC通信示意图
浏览器架构

不同的浏览器可能是多进程或者单进程的。



Chrome浏览器架构,使用多进程的架构

浏览器顶层是浏览器进程(Browser process),与浏览器其他应用模块进行协调工作。



Chrome浏览器各个进程的工作:

  • Browser Process:
  • 地址栏,书签栏,前进后退按钮等信息
  • 不可见的一些操作,请求,文件访问等
  • Rederer Process
  • 负责一个网页tab的的所有工作
  • Plugin Process
  • 插件进程,处理一个网页用到的插件,例如Flash的插件
  • GPU Process
  • 负责处理GPU相关的工作 *还有其他进程,例如:扩展应用,应用进程



不同进程指向浏览器 UI 的不同部分

谷歌浏览器每一个站点(网页)都是独立的进程,这样可以保证即使网页崩溃了,其他网页也不会受到影响。浏览器有多个进程的好处就是安全性跟跟沙箱化。由于操作系统提供了限制进程权限的方法,浏览器就可以用沙箱保护某些进程。由于进程有自己的私有内存空间,所以每个进程都有公共基础设施的拷贝,这也意味着会占用更多的内存,当运行达到极限时候,Chrome对于同一站点的不同标签页,会使用同一进程。



当Chrome运行在强力硬件上时,会把不同的服务功能模块分配到不同的进程,从而提升稳定性,但是当运行在弱硬件设备时候,会将一些服务功能模块整合到同一个进程以节约内存,但是相应的稳定性也会下降。



每个Iframe的渲染进程)——站点隔离

站点隔离实现每个Iframe运行在独立的渲染进程。每个tab站点单独运行一个进程,站点内的Iframe运行一个单独的渲染进程,这样在不同的站点内,相同Iframe可以共享内存。

同源策略是web的安全模型,也就是某一站点在没有授权的情况下,其他站点是不能获取其数据的。进程隔离是分离站点的最高效手段。



站点隔离示意图
导航时候发生了什么?

它以浏览器进程(Browser Process)开始

浏览器进程管理一切除Ta外之外的一切,浏览器进程内有很多线程,例如绘制浏览器的按钮和输入栏的UI线程、处理网络栈以从因特网获取获取的网络线程、控制文件访问的存储线程。当输入URL,输入由浏览器的进程的UI线程处理。



顶部是浏览器 UI,底部是拥有 UI、网络和存储线程的浏览器进程图
一个简单的导航
  1. 处理输入

浏览器进程的UI线程首先确定是搜索查询还是一个URL。UI线程决定是搜索内容到搜索引擎还是去一个网站。



UI 线程询问输入内容是搜索查询还是 URL 地址
  1. 当按下Enter键,UI线程启用网络去调取获取站点内容,加载动画会显示在标签页的一角。网络线程会通过适当的协议,为请求建立TLS连接。

SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。

SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。

安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面,与具体的应用无关,所以,一般把TLS协议归为传输层安全协议。



Roger[ˈrɑ:dʒə(r)]:无线通信答语:收到。UI 线程告诉网络线程要导航到 mysite.com

在这时,网络线程可能会收到像 HTTP 301 那样的服务器重定向头。这种情况下,网络线程会告诉 UI 线程,服务器正在请求重定向。然后,另一个 URL 请求会被启动。

  1. 读取响应



包含 Content-Type 的响应头以及作为实际数据的 payload

一旦开始收到响应主体,网络线程会查看数据流的前几个字节,响应报文的Content-Type字段会声明数据的类型。但可能存在丢失会错误。所以有了MIME类型嗅探来解决这个问题。

如果响应是一个HTML文件,那么下一步就会把数据传给渲染进程,如果是一个下载的文件,意味着是一个下载请求,会把它传递给下载器管理器。



网络线程询问一个响应数据是否是从安全网站来的 HTML

此时也会进行safeBrowsing检查,如果域名和数据匹配到一个恶意网站,那么网络线程会显示一个警告页面。此外也会进行Cross Origin Read Bloking (CORB)检查,以确保敏感的跨域数据不被传给渲染进程。

  1. 查找渲染进程

一旦所有的检查处理完毕并且网络线程确定会导航到请求的站点,网络请求线程会告诉UI线程所有的数据请求完毕。UI线程会寻找渲染进程开始渲染Web页面。



网络线程告诉 UI 线程去查找渲染进程

由于网络请求线程会花费时间,一次可以应用一个优化措施,当UI线程正发送一个URL请求给网络线程时候,可以同时查找开启一个渲染进程待命,如果导航重定向,这个渲染进程或许不会用到。

  1. 提交导航

现在数据和渲染进程就绪,浏览器会发送一个IPC(进程间通信)到渲染进程去提交导航,他也会传递数据流,所以渲染进程会保持接受HTML数据,一旦浏览器进程收到渲染进程已经提交的确认信息,导航完毕并且文档加载解析开始。

这时,地址栏已经更新,安全指示器和站点设置UI会放映新页面的站点信息,此标签页的session历史记录被更新,所以前进后退按钮会走向刚导航过的站点, 当你关闭标签页或者窗口,为了优化Tab/session的还原,session历史被保存在硬盘上。



浏览器和渲染进程间的 IPC,请求渲染页面。
额外的步骤:初始加载完毕

一旦导航别提交,渲染进程开始加载资源和渲染页面,一旦渲染进程渲染完毕,会发送IPC返回给浏览器进程,(这也会所有frameh和onload事件已经触发和执行完毕后发生)。这时,UI线程停止标签页上的加载动画。



导航到另一个站点

简单导航已经完成,在导航栏在输入一个URL时,浏览器进程会先检查已经渲染的站点是否关心beforeUnload事件,确认没有的话,就会执行相同的操作,导航到另一个站点。

befounload事件会在用户离开或者关闭标签页面时候给予提醒“离开此站点”。

注意:不要添加无条件的beforeunload处理程序,会产生延时。



浏览器进程向渲染进程发送 IPC 告诉它将要导航到另一个站点

如果渲染进程启动了导航(window.location.herf=xxx),渲染进程会先检查befoeload事件处理程序,会像浏览器处理导航一样执行相同的步骤,唯一不同的是导航请求是由渲染进程发送到浏览器进程的。

当新导航到新站点时,会调用一个独立的渲染进程来处理导航,同时保留当前的渲染进程来处理unload事件。



2 个 IPC(从浏览器进程到新渲染进程)告知渲染页面并告知旧渲染进程卸载
Service Worker待补充
渲染进程的内部机制
渲染进程处理网站内容

渲染进程负责标签内发送的所有事情。渲染进程中,主线程处理服务器返回给用户的大部分数据,如果使用了web sworker或Service Sorker,部分JS将由工作线程处理。合成和光栅线程也在渲染进程内运行,以高效流畅的呈现页面。

渲染进程的核心工作是将HTML,CSS和JavaScript转换为用户可以与之交互的网页。



渲染进程内部包含主线程、工作线程、合成线程和光栅线程
  1. 解析(Parsing)
  • Dom的构建 当渲染进程收到HTML数据时,主线程解析文本字符串(HTML)并将其转换为(DOM). DOM是页面在浏览器的内部表现,也是开发人员通过JS与之交互的数据结构

HTML的标签的解析由HTML Standar决定,HTML规范可以很优雅的处理一些标签错误。

  1. 子资源加载

网站的图像,CSS,JS外部资源,需要从网路或者缓存加载。解析构建DOM时,主线程会按处理顺序逐个加载,为了加快速度,“预加载扫描器(preload scanner)”会同时进行。如果文档有<img><link>,预加载扫描器会在浏览器进程中发送请求。



主线程解析 HTML 并构建 DOM 树

3.JS阻塞解析 当解析HTML时,遇到<script>时,会停止解析接下来的内容,加载js,并执行里面的代码。

  1. 提示浏览器如何加载资源

可以在<script>标签加async或者defer属性,实现异步加载JS,或者加载JS模块,可以使用 <link rel="preload">告知浏览器当前导航肯定需要该资源,并且你希望尽快下载。

  1. 样式计算

只有DOM元素无法确定页面的外观,需要结合CSS样式。主线程解析CSS并且确定每个DOM节点计算后的样式。



主线程解析 CSS 以添加计算后样式
  1. 布局树(Layout Tree)

知道每个节点已经对应的元素样式,不足以渲染页面。布局是计算几何元素形状的过程,主线程遍历DOM,计算样式并创建布局树,其中包含xy坐标和边框大小等信息,布局树可能与DOM树结构树类似,但它仅包含页面可见内容的信息。如果一个元素应用了 display:none,那么该元素不是布局树的一部分。类似地,如果应用了如 p::before{content:"Hi!"} 的伪类,则即使它不在 DOM 中,也包含于布局树中。



主线程遍历计算样式后的 DOM 树,以此生成布局树
  1. 绘制

有了DOM、样式、布局树还不能画出页面,还不知道绘制的顺序。 例如,有的元素有了z-index,简单从上到下绘制就会出现图层高低错误。



因为没有考虑z-index,页面元素按HTML标记的顺序出现,导致错误的渲染图像

在绘制步骤,主线程遍历布局树(Layout Tree)创建绘制记录(Paint Records),就像是背景优先,然后矩形,文字。



线程遍历布局树并生成绘制记录
  • 更新渲染管道的成本很高
DOM + Style、布局和绘制树的生成顺序

渲染管道最重要的事情:每个步骤,都是前一个操作的结果用于后一个操作的数据。如果为元素设置动画,每一帧都要进行相同的处理操作,大多数浏览器的1帧/秒 如果浏览器丢失了中间部分帧,就会让人觉得卡顿。



时间轴上的动画帧
  1. 真正绘制一个页面
  • 光栅化
简单光栅处理示意图

现在知道了文档的结构,CSS,布局树,绘制顺序。就是将数据转化为物理设备上的像素了。这个过程成为光栅化。

  • 合成

合成处理是将页面的各个部分光栅化,并且合成线程进行图层移动合成。

  • 分层

为了分清哪些元素位于什么图层,主线程遍历布局树创建图层树,如果某些部分是单独图层(例如划入式侧面菜单栏),但没有拆分出来,可以用CSS属性:will-change提示浏览器。



  • 主线程的光栅化和合成

一旦创建了图层树和确定了绘制顺序,主线程将会把信息传递给合成线程,接着,合成线程会光栅化每个图层,一个图层可能跟页面一样大,合成线程将其分块后发送给光栅线程。光栅线程光栅化每个小块后将他们存储在显存中。



光栅线程创建分块的位图并发送到 GPU

合成线程会不同的光栅化线程设置优先级,以便视图或者附近区域的画面可以先光栅化显示。图层还具有不同的分辨率的块,可以放大显示。

一旦块被光栅化,合成线程会收集这些块的信息(称为绘制四边形),创建合成帧。

  • 绘制四边形:包含块在内存的位置,以及合成时块在页面中的位置等信息。
  • 合成帧:一个绘制四边形的集合,代表一个页面的一帧。

接着,合成帧通过IPC提交给浏览器进程,此时,可以在UI线程或者其他插件的渲染进程添加一个合成帧,这些合成器帧被送到GPU然后在屏幕上显示。如果收到滚动事件,合成帧会创建另一个合成帧到GPU。

合成线程创建合成帧,将其发送到浏览器进程,再接着发送到 GPU



用户输入行为与合成器

用户的任何行为,对于浏览器来说都是输入行为。包括点击,滚动,触摸屏幕,滑动鼠标。

例如用户触摸屏幕时,浏览器进程率先捕捉行为,浏览器进程所掌握的信息仅限于行为发生的区域,因为标签内的内容都由渲染进程处理。浏览器进程会将点击行为以及坐标传达给渲染进程。渲染进程在进行相应的处理。



合成器接收输入事件



悬于页面图层的视图窗口
理解非立即可滚动区

运行JS是渲染进程的主线程的工作,页面合成之后,注册了事件的区域叫做“非立即可滚动区”,合成器线程会通知渲染进程的主线程处理。没有输入事件没有发生在事件注册区域,合成器进程则不需要等待主线程,可以继续合成帧。



设置时间处理应该注意
document.body.addEventListener('touchstart', 
event => {
    if (event.target === area) {
        event.preventDefault();
    }
});
复制代码

事件代理是浏览器常用的事件处理模式,在顶层元素添加一个事件。 这样带来的问题就是整个页面都被标识为非立即滚动区域,合成器进程需要每次都询问主线程是否需要处理事件并且等待反馈。流畅的合成器处理模式就失效了。



你可以给事件监听添加一个 passive:true 选项 ,将这种负面效果最小化。这会提示浏览器你想继续在主线程中监听事件,但合成器不必停滞等候,可接着创建新的合成帧。

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

不过上述写法可能又会带来另外一个问题,假设某个区域你只想要水平滚动,使用 passive: true 可以实现平滑滚动,但是垂直方向的滚动可能会先于event.preventDefault()发生,此时可以通过 event.cancelable 来防止这种情况。

document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // 阻止默认的滚动行为
        /*
        *  这里设置程序执行任务
        */
    } 
}, {passive:: true});
复制代码

也可以使用css属性 touch-action 来完全消除事件处理器的影响,如:#area{touch-action:pan-x;}

查找事件对象

当组合器线程发送输入事件给主线程时,主线程首先会进行命中测试(hit test),来查找对应的时间目标,命中测试会基于渲染过程中生成的绘制记录(paint records)查找事件发生坐标下寻找的元素。



主线程检查绘制记录查询坐标 x、y 处绘制内容

#####事件的优化

一般我们屏幕的刷新速率为 60fps,但是某些事件的触发量会不止这个值,出于优化的目的,Chrome 会合并连续的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延迟到下一帧渲染时候执行 。而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非连续性事件则会立即被触发。



使用 getCoalescedEvents 获取帧内事件

事件合并可帮助大多数 web 应用构建良好的用户体验。然而,如果你开发的是一个绘图类应用,需要基于 touchmove 事件的坐标绘制线路,那么在你试图画下一根光滑的线条时,区间内的一些坐标点也可能会因事件合并而丢失。这时,你可以使用目标事件的 getCoalescedEvents 方法获取事件合并后的信息。



window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // 使用 x、y 坐标画线
    }
});
复制代码

参考: