Web Worker 速度有多快?

下一版本的 Firefox OS,这款移动操作系统,将充分利用设备的多核处理器,释放它们的强大功能。传统上,JavaScript 只能在单个线程上执行,但 Web Worker 提供了一种并行执行代码的方法。这样做可以释放浏览器的负担,让它可以平滑地为 UI 动画提供服务。

Web Worker 简介

Web Worker 有多种类型

它们各自拥有特定的属性,但设计思路相似。在 Worker 中运行的代码在其独立的线程中执行,与主线程和其他 Worker 并行运行。不同类型的 Worker 共享相同的接口。

Web Worker

专用 Web Worker 由主进程实例化,它们只能与主进程通信。

共享 Worker

共享 Worker 可被运行在同一来源(不同的浏览器标签页、iframe 或其他共享 Worker)上的所有进程访问。

服务 Worker

服务 Worker 近期引起了广泛关注。它们使得通过编程方式代理 Web 服务器成为可能,从而向请求方(例如主线程)提供特定内容。服务 Worker 的一个用例是在离线时提供内容。服务 Worker 是一种非常新的 API,尚未在所有浏览器中完全实现,本文不会介绍它。

为了验证 Web Worker 是否能使 Firefox OS 运行更快,我们需要通过基准测试验证它们的性能。

创建 Web Worker 的成本

本文重点介绍 Firefox OS。所有测量均在 Flame 设备 上进行,该设备采用中端硬件。

第一组基准测试将查看创建 Web Worker 所需的时间。为此,我们设置了一个脚本,该脚本实例化一个 Web Worker 并发送一条最小的消息,Worker 会立即回复该消息。主线程收到回复后,会计算操作所需的时间。销毁 Web Worker 并重复操作足够多的次数,以了解创建功能性 Web Worker 平均需要多长时间。实例化 Web Worker 非常简单,如下所示:

// Start a worker.
var worker = new Worker('worker-script.js');

// Terminate a worker.
worker.terminate();

同样的方法适用于创建广播通道。

// Open a broadcast channel.
var channel = new window.BroadcastChannel('channel-name');

// Close a broadcast channel.
channel.close();

共享 Worker 在这里实际上无法进行基准测试,因为一旦创建,开发者就无法销毁它们。浏览器完全负责它们的生存期。因此,我们无法随意创建和销毁共享 Worker 以获得有意义的基准测试结果。

Web Worker 的实例化大约需要 **40 毫秒**。此外,这个时间非常稳定,只有几毫秒的偏差。设置广播通道通常在 **1 毫秒**内完成。

在正常情况下,浏览器 UI 的刷新频率为每秒 60 帧。这意味着任何 JavaScript 代码的运行时间不应超过一帧所需的时间,即 16.66 毫秒(每秒 60 帧)。否则,你的应用程序可能会出现卡顿和延迟。

实例化 Web Worker 效率很高,但仍然可能无法在一帧的分配时间内完成。因此,创建尽可能少的 Web Worker 并重用它们至关重要。

消息延迟

Web Worker 的一个关键方面是主线程与 Worker 之间具有快速通信。主浏览器线程可以通过两种不同的方式与 Web Worker 通信。

postMessage

此 API 是发送和接收 Web Worker 消息的默认方式,也是首选方式。 postMessage() 易于使用。

// Send a message to the worker.
worker.postMessage(myMessage);

// Listen to messages from the worker.
worker.onmessage = evt => {
  var message = evt.data;
};

广播通道

这是一个 新实现的 API,在撰写本文时,仅在 Firefox 中可用。它允许我们将消息广播到所有共享同一来源的上下文。所有来自同一来源的浏览器标签页、iframe 或 Worker 都可以发出和接收消息。

// Send a message to the broadcast channel.
channel.postMessage(myMessage);

// Listen to messages from the broadcast channel.
channel.onmessage = evt => {
  var message = evt.data;
};

为了进行基准测试,我们使用与上面描述的类似脚本,只是 Web Worker 不会在每次操作时被销毁和重用。获得往返回复所需的时间应该除以二。

正如你所料,简单的 postMessage 速度很快。发送消息通常需要 **0 到 1 毫秒**,无论是发送到 Web Worker 还是共享 Worker。广播通道 API 大约需要 **1 到 2 毫秒**。

在正常情况下,与 Worker 交换消息速度很快,你不必过于担心速度问题。但是,较大的消息可能需要更长的时间。

消息的大小

将消息发送到 Web Worker 有两种方式:

  • 复制消息
  • 传输消息

在第一种情况下,消息会被序列化、复制并发送。在第二种情况下,数据会被传输。这意味着,一旦发送,原始发送方将无法再使用它。传输数据几乎是即时的,因此对它进行基准测试没有实际意义。但是,只有 ArrayBuffer 是可传输的。

正如预期的那样,序列化、复制和反序列化数据会给消息传输增加相当大的开销。消息越大,发送时间越长。

这里的基准测试向 Web Worker 发送一个类型化数组。在每次迭代中,它的尺寸都会逐渐增加。消息尺寸与传输时间之间存在线性关系。对于每次测量,我们可以将尺寸(以千字节为单位)除以时间(以毫秒为单位),得到以 kb/ms 为单位的传输速度。

通常,在 Flame 上,postMessage 的传输速度为 **80 kB/ms**,使用广播通道的传输速度为 **12 kB/ms**。这意味着,如果你想让你的消息在一帧内完成传输,使用 postMessage 时应将其限制在 **1,300 kB** 以下,使用广播通道时应将其限制在 **200 kB** 以下。否则,你的应用程序可能会出现帧丢失。

在这个基准测试中,我们使用类型化数组,因为它可以精确地确定它们的尺寸(以千字节为单位)。你也可以传输 JavaScript 对象,但由于序列化过程,它们需要更长的时间才能发送。对于小型对象来说,这无关紧要,但如果你需要发送大型对象,你也可以将它们序列化为二进制格式。你可以使用类似于 Protocol Buffer 的工具。

正确使用 Web Worker 可以提高速度

以下是与 Web Worker 相关的各种基准测试的快速总结,这些测试是在 Flame 上进行的。

操作
实例化 Web Worker 40 毫秒
实例化广播通道 1 毫秒
使用 postMessage 的通信延迟 0.5 毫秒
使用广播通道的通信延迟 1.5 毫秒
使用 postMessage 的通信速度 80 kB/ms
使用广播通道的通信速度 12 kB/ms
使用 postMessage 的最大消息尺寸 1,300 kB
使用广播通道的最大消息尺寸 200 kB

基准测试是确保你正在实现的解决方案快速有效的唯一方法。这个过程可以消除 Web 开发中的许多猜测。

如果你想在特定设备上运行这些基准测试,我创建的用于进行这些测量的应用程序,Web Worker 基准测试,是开源的。你也可以通过提交新的基准测试类型来贡献你的力量。

关于 Guillaume Cedric Marty

Guillaume 在 Web 行业工作了十多年。他热衷于 Web 技术,并定期为开源项目做出贡献,他在他的 技术博客 上分享了这些项目的相关信息。他还对电子游戏、动画以及日语(作为日语使用者)等外语充满热情。

更多由 Guillaume Cedric Marty 撰写的文章…


9 条评论

  1. Fernando Montoya

    Guillaume,好文章!

    附:在服务 Worker 简介中,描述似乎重复了。

    2015 年 7 月 2 日 下午 11:40

  2. Luke

    非常实用……不幸的是,如果 DOM 操作占用了大量时间,就很难优化,因为 Web Worker 无法从线程访问 UI。

    2015 年 7 月 3 日 上午 12:26

    1. Guillaume Cedric Marty

      当然,Web Worker 并不是所有问题的解决方案,必须谨慎地将其用于特定的用例。

      2015 年 7 月 3 日 上午 1:57

    2. smaug

      Luke,是 DOM 操作真的在占用时间,还是更像是
      布局?
      如果你有任何 DOM 成为瓶颈的真实案例,请提交错误报告并抄送我 (:smaug)。

      2015 年 7 月 3 日 下午 12:04

  3. 3y3

    嗯……“实例化 Web Worker”并不具有代表性。如何看待
    var blob = new Blob(
    [‘console.log(“Hello, World!”)’],
    {type: ‘application/javascript; charset=utf-8’}
    );
    var worker = new Worker(window.URL.createObjectURL(blob));

    2015 年 7 月 3 日 上午 4:05

  4. Robert O’Callahan

    我们应该能够将启动 Worker 的主线程延迟降低很多。如果这很重要,请提交错误报告。

    2015 年 7 月 3 日 下午 10:21

  5. Gervase Markham

    “通常,在 Flame 上,postMessage 的传输速度为 45 kb/ms,使用广播通道的传输速度为 6b/ms。这意味着,如果你想让你的消息在一帧内完成传输,使用 postMessage 时应将其限制在 350MB 以下,使用广播通道时应将其限制在 50MB 以下。”

    如果一帧是 16.66 毫秒,那么这里的数学计算似乎有点问题……(即使你假设“6b/ms”指的是“6kb/ms”。)

    45 * 16 = 720kb 每帧,而不是 350MB 每帧……

    2015 年 7 月 4 日 上午 8:54

    1. Guillaume Cedric Marty

      感谢你指出这一点,我在这里犯了两个错误。首先,这些值是往返的,因此速度必须乘以 2,因为我们只关心单向通信。然后,由于代码中的错误重构,最大消息尺寸的单位被错误地乘以了 1024。
      我已经修复了这篇文章。如果还有其他不清楚的地方,你可以查看应用程序的代码 (https://github.com/gmarty/web-workers-benchmark)。

      2015 年 7 月 6 日 下午 04:07

  6. El Goopo

    在 Web Workers 能够创建可以断开连接并传递回主线程的文档(片段)之前,它们在网络中的使用范围仍然有限。我们现在有了离线线程功能,但这对大多数 Web 应用程序来说似乎并没有帮助,真是令人失望。Canvas 和 Audio worker 固然不错,但 DOMWorker 才是真正的改变游戏规则的力量。在主线程上构建和传递字符串,然后再重新解析它们,实在太荒谬,而且效率低下。即使只是拥有 DOM 功能的子集,也会很棒。

    2015 年 7 月 8 日 下午 17:33

此文章的评论已关闭。