我们都听说过
现代网站设计中的一条神圣规则——永不可违背的规则。“绝不屏蔽主线”的规则。见的经验法则是,运行JavaScript任务时绝不要“屏蔽”浏览器的主线程。作为网页开发者,你几乎不会忽视它;几乎所有性能指南里都有,公平地说,这是个好建议。我们都知道浏览器的主线程是单线程的,意味着它一次只能做一件事。

正如我们所知,主线不仅仅是我们的;我们会把它分享给浏览器的渲染引擎、输入处理程序以及其他关键任务。因此,我们对主线程停留的时间越少,应用的响应性就越高。这导致我们与后台工作人员共享任务,因为我们说服自己UI和任何计算之间应该有一条明确的界限,这条界限绝不能被跨越。
几个月前我在开发一个带有截图功能的 Chrome 扩展 Fastary 时发现了这一点。我在所有测试中发现延迟大约只有2到3秒,即使使用了屏外文档(Chrome扩展中的一个后台进程)来处理画布操作。毕竟截图任务应该感觉瞬间完成,没有延迟。很讽刺的是,出于反射性,我们会把工作从主线程移开以避免界面冻结,但有时移动这些工作(比如序列化、复制和反序列化)也会冻结界面。有时候推荐的让背景完成工作的方式,可能比单纯做主线程的工作慢。
为了让大家更清楚,让我们理解为什么要隔离浏览器上下文以及它们如何相互通信,重点是通信部分。浏览器不仅仅是一个环境。不同的环境同时运行,每个环境都有自己的内存空间、可访问的内容和规则:主线是我们最熟悉的;这里运行 JavaScript 逻辑,DOM 所在,样式渲染,用户互动。Web Worker 是独立的线程,也可以在没有 DOM 访问的情况下执行 JavaScript。我们主要用它处理大量数据任务。服务工作者是网络相关的代理,负责拦截网络请求,甚至在页面关闭时也能运行。还有Chrome扩展上下文,我们有后台服务工作者、内容脚本和屏幕外文档。
这些都与其他人隔离开来。Web Worker 或后台脚本存在于与主线程不同的内存空间中。他们不能直接读取对方的变量或逻辑,这被称为“共享无”架构。postMessage()告诉浏览器将数据发送到请求的上下文中。但为此,浏览器依赖于结构化克隆算法(SCA)。SCA类似,但更强大、更聪明。在最简单的形式中,SCA是一种深度递归复制操作,即克隆。它会遍历所获得的整个数据结构,克隆每一个值,将其序列化为可传输格式,将这些字节发送到目标上下文,然后在接收端重建原始对象。SCA很快,或者说还算快......对于像 这样的小型常规配置对象,它是不可察觉的;你甚至都没注意到。然而,处理大量数据时情况会不同,因为SCA是一种同步分区操作,也就是说,成本随着数据大小线性增加。让我们把这件事放在一个整体的视角里。用户只需点击按钮,内部会发送一个8MB的图像负载到后台工作者进行处理。当你调用 时,主线程必须立即停止正在进行的操作,以运行该序列化和复制过程。
真正追求超高性能Web应用的开发者通常会使用可转移对象(例如,、、或)来绕过结构化克隆算法。这是因为当你转移一个对象时,你并不是在复制(像SCM)。相反,浏览器会将数据的所有权从一个上下文切换到另一个上下文。浏览器执行切换,发送上下文会立即失去对数据的访问,接收上下文完全控制。它实际上非常快。根据 Chrome 开发者基准测试,传输一个庞大的 32MB 数据不到 7 毫秒,而用 SCM 克隆时大约需要 300 毫秒。那是43倍的速度提升。
我们为什么还要费心去隔离语境?为什么不把所有问题都留给主线呢?将长时间运行的CPU任务卸载到后台线程绝对是正确的做法。浏览器需要每16.6毫秒绘制一帧以保持流畅;这意味着任何耗时>50毫秒的任务通常都被视为“长”。把内容移到背景绝对是正确的做法。然而问题是,我们把“绝不阻挡主线程”变成了绝对规则,却没有问过这项任务是处理成本高还是搬运成本高。