现代浏览器内部工作原理
汇总图
计算机的核心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、网络和存储线程的浏览器进程图
一个简单的导航
- 处理输入
浏览器进程的UI线程首先确定是搜索查询还是一个URL。UI线程决定是搜索内容到搜索引擎还是去一个网站。
UI 线程询问输入内容是搜索查询还是 URL 地址
- 当按下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 请求会被启动。
- 读取响应
包含 Content-Type 的响应头以及作为实际数据的 payload
一旦开始收到响应主体,网络线程会查看数据流的前几个字节,响应报文的Content-Type字段会声明数据的类型。但可能存在丢失会错误。所以有了MIME类型嗅探来解决这个问题。
如果响应是一个HTML文件,那么下一步就会把数据传给渲染进程,如果是一个下载的文件,意味着是一个下载请求,会把它传递给下载器管理器。
网络线程询问一个响应数据是否是从安全网站来的 HTML
此时也会进行safeBrowsing检查,如果域名和数据匹配到一个恶意网站,那么网络线程会显示一个警告页面。此外也会进行Cross Origin Read Bloking (CORB)检查,以确保敏感的跨域数据不被传给渲染进程。
- 查找渲染进程
一旦所有的检查处理完毕并且网络线程确定会导航到请求的站点,网络请求线程会告诉UI线程所有的数据请求完毕。UI线程会寻找渲染进程开始渲染Web页面。
网络线程告诉 UI 线程去查找渲染进程
由于网络请求线程会花费时间,一次可以应用一个优化措施,当UI线程正发送一个URL请求给网络线程时候,可以同时查找开启一个渲染进程待命,如果导航重定向,这个渲染进程或许不会用到。
- 提交导航
现在数据和渲染进程就绪,浏览器会发送一个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转换为用户可以与之交互的网页。
渲染进程内部包含主线程、工作线程、合成线程和光栅线程
- 解析(Parsing)
- Dom的构建 当渲染进程收到HTML数据时,主线程解析文本字符串(HTML)并将其转换为(DOM). DOM是页面在浏览器的内部表现,也是开发人员通过JS与之交互的数据结构
HTML的标签的解析由HTML Standar决定,HTML规范可以很优雅的处理一些标签错误。
- 子资源加载
网站的图像,CSS,JS外部资源,需要从网路或者缓存加载。解析构建DOM时,主线程会按处理顺序逐个加载,为了加快速度,“预加载扫描器(preload scanner)”会同时进行。如果文档有<img><link>
,预加载扫描器会在浏览器进程中发送请求。
主线程解析 HTML 并构建 DOM 树
3.JS阻塞解析 当解析HTML时,遇到<script>
时,会停止解析接下来的内容,加载js,并执行里面的代码。
- 提示浏览器如何加载资源
可以在<script>
标签加async或者defer属性,实现异步加载JS,或者加载JS模块,可以使用 <link rel="preload">
告知浏览器当前导航肯定需要该资源,并且你希望尽快下载。
- 样式计算
只有DOM元素无法确定页面的外观,需要结合CSS样式。主线程解析CSS并且确定每个DOM节点计算后的样式。
主线程解析 CSS 以添加计算后样式
- 布局树(Layout Tree)
知道每个节点已经对应的元素样式,不足以渲染页面。布局是计算几何元素形状的过程,主线程遍历DOM,计算样式并创建布局树,其中包含xy坐标和边框大小等信息,布局树可能与DOM树结构树类似,但它仅包含页面可见内容的信息。如果一个元素应用了 display:none,那么该元素不是布局树的一部分。类似地,如果应用了如 p::before{content:"Hi!"} 的伪类,则即使它不在 DOM 中,也包含于布局树中。
主线程遍历计算样式后的 DOM 树,以此生成布局树
- 绘制
有了DOM、样式、布局树还不能画出页面,还不知道绘制的顺序。 例如,有的元素有了z-index,简单从上到下绘制就会出现图层高低错误。
因为没有考虑z-index,页面元素按HTML标记的顺序出现,导致错误的渲染图像
在绘制步骤,主线程遍历布局树(Layout Tree)创建绘制记录(Paint Records),就像是背景优先,然后矩形,文字。
线程遍历布局树并生成绘制记录
- 更新渲染管道的成本很高
DOM + Style、布局和绘制树的生成顺序
渲染管道最重要的事情:每个步骤,都是前一个操作的结果用于后一个操作的数据。如果为元素设置动画,每一帧都要进行相同的处理操作,大多数浏览器的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 坐标画线
}
});
复制代码
参考: