浏览器架构的温故知新
【引子】前端可能是一个日新月异的领域,我们很难了解其中的方方面面。但是,前端系统一般都以浏览器作为运行环境, 对浏览器的进一步理解有助于我们更好地开发前端应用。这也是本文的由来之一,也作为对runtime的一次实例分析。
浏览器架构经历了从单进程浏览器到多进程浏览器的转变。在强调稳定性、平滑性和安全性的同时,进程开始分解为渲染、 GPU、网络和插件等,提高了架构的整洁性。回顾浏览器的架构,需要进一步了解打开页面的过程、页面渲染的过程以及浏览器插件机制。通过整理 Chrome 插件版本的时间表,特别是从 Manifest V1到 Manifest V3的转换,可以对浏览器随时间变化有一个相对全面的理解。
1. 浏览器架构的演变
2007年之前的浏览器架构一般是这样的:
单进程浏览器架构在单个操作系统进程中运行整个 Web 浏览器,将网络处理、插件、 JavaScript 运行时、呈现引擎、页面管理和用户界面元素等任务集中在一个执行空间中。在简化资源管理的同时,这种架构设计主要问题有:
-
不稳定ーー插件和处理 Web 视频和游戏等功能的渲染引擎在同一进程中运行。插件或渲染引擎中的崩溃可能导致整个浏览器崩溃,这种不稳定性在处理复杂的 JavaScript 代码时尤其明显。
-
不平滑ーー所有模块(包括页面呈现、 JavaScript 执行和插件)都共享一个线程。如果一个脚本变得非常耗时,它就会垄断整个线程,导致其他页面无法响应,并且整个浏览器会出现延迟。
-
不安全ーー在页面上运行的插件可以访问操作系统资源。恶意插件可以利用这种访问来释放病毒,危害安全性,并可能窃取用户凭据等敏感信息。
单进程浏览器的优势是在一个进程中操作的所有浏览器组件简化了资源管理和协调。单进程浏览器通常表现出较低的内存使用率,有利于资源效率的提升。在一个统一的过程中,任务在同一个过程中按顺序运行。
2008年发布的 Chrome 进程架构是一个多进程浏览器,如下图所示:
早期的浏览器架构将功能划分为三个主要进程程: 浏览器、插件和渲染。每个页面及其插件在专用的渲染和插件进程中独立运行,通过 IPC 进行通信。
进程间通信(IPC)是一种机制,使进程能够在计算机上进行通信和同步操作。它促进了不同程序之间有效的数据交换和协调。关键的 IPC 机制包括共享内存,允许进程通过信号量来同步访问共享的公共内存区域。命名和非命名管道提供了单向通信,Linux 中的 IPC 通常涉及通过共享文件或带信号量的内存共享存储。消息队列支持异步通信,有助于分离发送方和接收方进程。此外,进程可以通过信号进行通信,相互通知特定的事件或请求。socket利用网络协议将 IPC 扩展到不同的机器。
多进程浏览器增强了稳定性,隔离进程可以防止崩溃影响到整个浏览器。页面或插件崩溃只会影响其特定的进程,从而确保了其他页面和浏览器的稳定性。同时,在呈渲染进程中运行 JavaScript 也可以隔离其影响。如果脚本阻塞呈现进程,它只影响当前页,浏览器和其他页不受影响,因为每个页都在其专用渲染进程中运行脚本。另外,Chrome 将插件和渲染进程放在沙箱环境中,限制了数据的读写访问。即使恶意程序在渲染或插件进程中执行,它也不能破坏沙箱以获得系统权限。这是舱壁架构模式的一个具体体现。
沙箱是一个测试环境,它允许用户在不影响整个系统的情况下运行程序或打开文件。在网络安全领域,沙箱分析并执行潜在的恶意代码,检测并减轻威胁。
在近期的 Chrome 浏览器中,其架构由关键进程组成,如下图所示:
浏览器进程管理界面显示、用户交互、子进程协调和提供存储功能。它充当协调其他进程的“调度器”,例如在输入 URL 时调用网络进程。渲染过程将 HTML、 CSS 和 JavaScript 转换为交互式网页,运行 V8引擎。为了安全起见,Chrome 在沙箱模式下为每个选项卡创建了一个单独的渲染进程。
GPU 进程最初是为了3D CSS 效果,后来扩展到绘制网页和 Chrome UI 界面。在 Chrome 的多进程架构中引入,以满足常见的浏览器需求。网络进程独立加载页面网络资源,最初是浏览器进程中的一个模块,现在作为独立进程运行。插件进程ーー管理插件以防止由于插件固有的不稳定性而导致的崩溃影响浏览器和页面。
现代浏览器架构如下图所示:
Chrome 已经采用了面向服务的浏览器架构 ,旨在基于不同的硬件能力提高灵活性和优化性能。主要目标是将与浏览器(Chrome)相关的组件划分为不同的服务,这些功能可以在单独的进程中运行,也可以合并为单个进程。
这种转变背后的主要动机是根据不同硬件的性能来定制 Chrome 的性能。在强大的硬件上,与浏览器进程相关联的服务在单独的进程中运行。在功能不太强大的硬件上,这些服务在相同的进程中运行,有效地减少了内存使用。
2. 浏览器页面打开的背后
-
添加选项卡将启动基本进程的创建: 系统浏览器、渲染、 GPU 和网络进程。
-
用户输入触发浏览器进程来检查、组装协议并形成完整的 URL。
-
浏览器进程通过进程间通信将 URL 请求传送给网络进程。
-
网络进程检查请求的资源的本地缓存,如果找到该资源,则将其返回给浏览器进程。
-
如果不在缓存中,网络进程将向 Web 服务器发起一个 HTTP 请求。
-
网络进程解析响应,检查状态码,非200状态码提示特定的处理逻辑。
-
对于200响应,浏览器进程将检查 Content-Type。字节流触发下载管理器,而 HTML 则发出准备渲染的信号。
-
浏览器进程检查当前 URL 是否与现有呈现进程的根域匹配。如果不同,则启动新的渲染进程。
-
浏览器向渲染进程发出“提交文档”消息,与网络进程建立一个数据传输管道。
-
接收数据后,渲染进程向浏览器发送一个确认。浏览器更新界面状态,包括安全性、地址栏 URL、浏览历史和网页。
3. 渲染进程
现代浏览器使用了延迟加载和缓存等策略来优先考虑性能。浏览器通过渲染进程来显示 Web 内容。关键阶段包括 HTML 解析、 CSS 样式设计、布局创建和绘制,具体步骤如下:
-
用户输入ーー在浏览器的地址栏中输入 URL。
-
URL 解析ーー解析 URL 以标识协议、主机、端口和路径。
-
DNS 查找ーー通过 DNS 查找将主机名转换为 IP 地址。
-
套接字连接ーー在用户和服务器 IP 之间建立连接。
-
HTTP 请求ーー向服务器发送 HTTP 请求,并指定协议。
-
服务器处理ーー服务器评估请求,确定处理插件(例如 PHP、 Java)。
-
通过插件处理ーー访问数据库或其他资源作为 HTTP 响应的一部分。
-
响应浏览器ーー将 HTTP 响应发送回浏览器。
-
响应分析ーー浏览器从响应中分析 HTML 数据。
-
DOM 树创建ーー从解析的 HTML 构建文档对象模型(DOM)树。
-
样式表解析ーー解析样式表,将呈现数据链接到 DOM 节点。
-
JavaScript 执行ーー执行 JavaScript 代码,修改 DOM 。
-
页面渲染ーー使用 DOM 和样式数据显示网页。
3.1 HTML 解析
浏览器一个字符一个字符地读取 HTML,标识元素、属性和文本,然后构建表示网页结构的 DOM 树,并确保正确显示 HTML 代码。
3.2 CSS 对象模型
CSS 对象模型表达了应用于 HTML 元素的样式,类似于 DOM 树的结构化层次结构,并考虑了样式的特殊性和级联性,允许访问、操作和计算样式。
3.3 布局管理器
布局管理器结合 DOM 和 CSS 对象模型形成渲染树,根据内容、填充等确定Box的尺寸,使用各种方法构建具体位置。同时,使用堆叠上下文和 Z 索引处理重叠元素,使用批处理等技术来优化布局变更。最后,在屏幕上绘制元素,在用户交互期间不断更新。
4 插件机制
当使用插件时,浏览器的操作比普通网页还要简单。渲染过程负责运行网页,打开页面时,contentscript.js被加载并注入到网页环境中,操作类似于 JavaScript,操作 DOM 树并改变显示。GPU 进程支持渲染插件接口的硬件功能,网络进程管理插件中的外部资源请求,例如,插件依赖于外部 的JS 资源。同时,存储进程为插件提供了本地存储功能,使用chrome.storage.local在chrome扩展中本地存储和检索数据。浏览器进程起到了桥梁的作用,促进了Extension Page和contentscript.js之间的通信。
4.1 插件的发展历程
插件机制的发展过程如下:
-
2012年8月ー Manifest V1:Chrome 插件最初基于 Manifest V1,定义了基本的功能和权限。
-
2013年4月ー Chrome26稳定版:包括了对 Manifest V1插件的支持。
-
2014年5月ー Chrome35稳定版:浏览器的增量更新继续支持 ManifestV1插件。
-
2014年9月ー Chrome 37稳定版:Manifest V1插件的进一步改进和 bug 修复。
-
2015年5月ー Chrome43稳定版:继续支持Manifest V1。
-
2015年12月ーManifest V2:引入了 ManifestV2,带来了更好的安全性和附加功能。
-
2016年6月ー Chrome51稳定版:Manifest V2成为插件开发的标准。
-
2016年9月ー Chrome 53稳定版:Manifest V2 的持续改进和优化。
-
2019年1月ーManifest V3 诞生:重点关注安全性、性能和开发的灵活性。
-
2020年3月ー Chrome80稳定版:Manifest V2仍然是插件的标准。Manifest V3可用于测试,但还不是必需的。
-
2021年3月ー Chrome89稳定版:Manifest V2 仍然是默认的,但是Manifest V3开始受到关注。
-
2021年10月ー Chrome94稳定版:Manifest V3开始对一些特性进行强制执行,并为开发人员提供了迁移指南和工具。
-
2022年3月ー Chrome98稳定版:清单 V3成为新插件的默认版本,并继续支持清单 V2。
-
2022年8月ー Chrome104稳定版:所有插件完全转换到 Manifest V3,正式宣布不支持 Manifest V2。
-
2023年3月ー Chrome108稳定版:保持了对 Manifest V3的完全支持,确保了所有插件的平稳过渡。
-
2023年7月ーー Manifest V3预览:允许开发人员探索即将发生的变化并提供有价值的反馈。
-
2023年10月ー Chrome112稳定版进一步完善了对 Manifest V3的支持,解决了预览阶段报告的所有问题。
-
January 2024 — Manifest V3 发布预稳定版本:Manifest V3 达到了一个稳定的状态,鼓励开发人员将他们的扩展迁移到 V3,并提供了全面的文档和迁移指南。
-
2024年3月ー Chrome116稳定版:完全支持 Manifest V3,开发者更新他们的插件以确保与最新标准的兼容性。
总体而言,Chrome 插件(也被称为扩展)已经经历了3个主要版本的版本开发: Manifest V1、 Manifest V2和 Manifest V3。
Manifest V1 (MV1)是 Chrome 扩展清单的初始版本,已经被放弃。Manifest V2 (MV2)是当前 Chrome 扩展中广泛使用的主流版本,它提供了一个健壮的框架,用于构建具有增强浏览器功能的特性和功能的扩展。Manifest V3是最新的版本,正在逐步取代 MV2。引入 MV3是为了解决安全性和性能方面的问题,它强化了更强的安全措施,并促进了扩展开发中的更好性能。从 Chrome 127开始(2024年6月) ,谷歌开始在预稳定版本的 Chrome 中禁用 Manifest V2扩展,鼓励开发者转向 MV3。
4.2 Manifest 的 功能特点与版本迁移
Manifest V2 的功能特性:
-
使用 script-src‘ self’; object-src‘ self’; Content-Security-Policy (CSP) 设置默认内容安全策略。
-
插件包资源不再可用于外部; 通过清单的 web _ access _ resources 属性列出白名单。
-
浏览器操作 API 和页面操作 API 发生了更改, chrome.extension 代替了 chrome.self 指向插件本身,chrome.tension.getTabContentses被 tension.getView 所替代,Port.tab替换为runtime.Port等
Manifest V3 的功能特性:
-
Manifest V3 引入了Service worker,,取代了后台页面。
-
网络请求修改使用了新的声明文件请求 API,而不是已经废弃的 webRequest API。
-
不允许远程代码执行; 只有扩展包中的 JS 可以运行。
-
Promises 被添加到许多方法中,并且仍然支持回调。
-
Browser Action API 和 Page Action API 统一为单独的 Action API。
-
Web 可访问的资源仅限于指定的站点和扩展。
-
内容安全策略(CSP)允许为不同的执行上下文指定单独的 CSP,executeScript只能执行脚本文件和函数,不允许任意字符串。
Manifest V3代表了从 V1和 V2的重大转变,受到 Chrome 致力于提高隐私、安全性和扩展的整体性能的驱动。与之前的版本不同,Manifest V3优先考虑资源利用率,解决了人们对 Chrome 历史性的高资源利用率的担忧。其核心目标是通过扩展来限制系统资源消耗,以优化浏览器性能。在施加额外限制的同时,Manifest V3引入了显著的好处。ServiceWorker 功能允许扩展操作,而无需一直驻留在后台。这样可以回收扩展资源,有效地减少总体浏览器开销。对规则计算的限制作为一种控制机制,确保单个扩展不会过度消耗资源。这些改变共同促进了 Chrome 浏览器更加流畅的体验,符合用户对提高浏览器效率的期望。
在从V2迁移到V3的时候,由于缺少用于配置页面背景的 background. html,与 V2版本不同的是,windows 对象上的 XMLHttpRequest 不再适用于 background. html 来构造 AJAX 请求。相反,必须利用提取方法来获取接口数据。
另外,由于service workers 的生命周期很短,并且在非活动期间终止,因此他们在整个插件生命周期中偶尔启动、运行和终止,从而引入不稳定性。在 MV2中,全局变量被用来直接存储数据。为了适应这种情况,需要对 backound.js 中的逻辑进行修改,以提高稳定性和功能性。而且,从 webRequest API 过渡到 statativeNetRequest API 需要大量的代码重构。
4.3 Chrome 插件的主要构成
4.3.1 manifest 文件
manifest.json 文件对于位于根目录中的 Chrome 插件非常重要。它用于配置所有插件设置,其基本参数为 Manif_ version、 name 和 version。
Manifest V2 的一个示例如下:
{
"manifest_version": 2,
// Plugin name
"name": "...",
// Plugin version
"version": "1.0.0",
// Plugin description
"description": "...",
"icons": {
"16": "img/icon16.png",
"48": "img/icon48.png",
"128": "img/icon128.png"
},
// Persistent background JS or background page
"background": {
"scripts": ["js/background.js"]
},
// Browser icon settings :browser_action, page_action, app
"browser_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
},
// Icon displayed only when specific pages are open
"page_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
},
// JS directly injected into pages
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["js/content-script.js"],
"css": ["css/custom.css"],
// Code injection timing, default is document_idle
"run_at": "document_start"
}],
// Permissions requested
"permissions": [
"contextMenus", // Right-click menu
"tabs", // Tabs
"notifications", // Notifications
"webRequest", // Web requests
"webRequestBlocking",
"storage", // Plugin local storage
"https://*/*" // Websites accessible via executeScript or insertCSS
],
// List of plugin resources directly accessible by normal pages "web_accessible_resources": ["js/inject.js"],
"homepage_url": "...", // Plugin homepage
"chrome_url_overrides": { // Override browser default pages
"newtab": "newtab.html"
},
"options_ui": { // Plugin options page
"page": "options.html",
"chrome_style": true
},
"omnibox": { "keyword" : "..." }, // Register a keyword in the address bar for search suggestions, only one keyword can be set
"default_locale": "en", // Default language
"devtools_page": "devtools.html", // Devtools page entry, can only point to an HTML file "content_security_policy": "...", // Security policy
"web_accessible_resources": [ // Loadable resources
"RESOURCE_PATHS"
]
}
Manifest V3的一个示例如下:
{
"manifest_version": 3,
"name": "...",
"version": "1.0.0",
"description": "...",
"icons": {
"16": "img/icon16.png",
"48": "img/icon48.png",
"128": "img/icon128.png"
},
"background": {
"service_worker": "js/background.js"
},
"action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
},
"content_security_policy": {
"extension_pages": "...",
"sandbox": "..."
},
"web_accessible_resources": [
{
"resources": ["RESOURCE_PATHS"]
}
],
"permissions": [
"contextMenus",
"tabs",
"notifications",
"webRequest",
"webRequestBlocking",
"storage",
"https://*/*"
],
"web_accessible_resources": ["js/inject.js"],
"homepage_url": "...",
"chrome_url_overrides": {
"newtab": "newtab.html"
},
"options_ui": {
"page": "options.html",
"chrome_style": true
},
"omnibox": {
"keyword": "..."
},
"default_locale": "zh_CN",
"devtools_page": "devtools.html",
"content_security_policy": "...",
"web_accessible_resources": ["RESOURCE_PATHS"]
}
4.3.2 内容脚本
Chrome 插件中的内容脚本通过配置将 JS 和 CSS 注入到指定的页面中。它们与原始页面共享 DOM,但不与 JS 共享。访问页面 JS 变量需要注入 JS。内容脚本无法访问大多数 Chrome API,除了:
* chrome.extension
* chrome.i18n
* chrome.runtime
* chrome.storage
对于其他 API,需要与后台或service worker进行通信。
4.3.3 后台脚本
Chrome 扩展中的后台脚本具有最长的生命周期,并且在浏览器打开时连续运行。它拥有广泛的权限,允许访问大多数 Chrome 扩展 API 和跨源请求,而不受 CORS 限制。在 Manifest V3中,后台页被具有较短生命周期和基于事件的执行的服务工作者所替代,这使得它们不适合存储全局变量。
4.3.4 弹窗
弹出窗口是一个小窗口的网页,出现在点击右上角的图标。当用户在网页之外进行互动时,它会迅速关闭。通常用于临时交互,其权限级别类似于背景,但具有较短的生命周期。
4.3.5 注入脚本
开发者在 Chrome 插件开发过程中创造了“注入脚本”这个术语。它表示通过 DOM 操作注入到页面中的 JavaScript。内容脚本虽然能够操作 DOM,但由于访问限制,DOM 不能直接调用它。这种限制在事件绑定中是显而易见的。为了满足在 Web 页面中添加一个按钮来触发插件的常见需求,大家采用了插入脚本。
4.4 Chrome 插件的通信机制
在 Chrome 插件中,通信依赖于五种类型的脚本:
-
注入脚本,表示动态注入到网页中的脚本,通常依赖于 window.postMessage。
-
内容脚本,在特定网页上下文中执行的脚本,利用 window.postMessage、 chrome.runtime.sendMessage 和 chrome.runtime.connect 进行脚本间通信。
-
弹窗脚本,与插件的弹出式界面相关联,通常使用 chrome.tabs.sendMessage 和 chrome.tabs.connect 进行通信。
-
后台脚本,在后台独立运行的脚本,涉及多种方法,例如 chrome.tabs.sendMessage、 chrome.tabs.connect、 chrome.tension.getBackoundPage 和 chrome.tension.getView。
-
DevTools ,开发工具的附加功能, 使用诸如 chrome.devtools.spectedWindow.eval 和 chrome.runtime.sendMessage 之类的特定 API 进行交互。
每个脚本拥有不同的权限,强调了它们之间通信的重要性。这种交互对于启用广泛的插件功能非常重要。
5. 一句话小结
温故而知新,浏览器架构作为现代互联网的基石,历经多次迭代与创新,始终承载着用户与网页内容之间的桥梁作用。回顾其发展历程,从早期的单一渲染引擎到如今的多进程、多线程架构,每一次变革都带来了更为流畅、安全的浏览体验。展望未来,浏览器架构将继续深化其性能优化与安全性提升,为用户带来更加出色的网络浏览体验。
【关联阅读】