Day22 X Web Workers

身为前端开发者,整日与 JavaScript 这门程序语言打交道,应该都知道它是一个 single-threaded 的程序语言,刚开始会觉得有点诧异,现在的电脑硬体大多数都是多核心多执行绪的,JavaScript 这样的设计对於程序执行不会产生什麽大问题吗?

不过其实看下来大部分状况 JavaScript 还是能够满足程序执行的需求,这可以归类为几个原因:

  • JavaScript 原本设计时就预设执行环境是在浏览器上,每个浏览器只会有一个使用者,所以在这样的前提下是一个合理的设计。

  • 虽然说 JavaScript 的「程序执行」是 single threaded 的,但浏览器或是 server side 的执行环境却不是,我们看到的是只有一个 thread 在执行程序,然而实际上在执行环境中还有其他 threads 在背後辅助程序码的执行。

  • 大部分都是需要等待的 I/O 操作,例如 call API 去 query 资料其实繁杂的操作都在 DB 那里,并不是在 JavaScript 程序中执行查询,这些异步操作在 Event Loop 架构下也被妥善处理,达成异步 non-blocking 的特性。(不懂 Event Loop 机制的读者建议一定要去理解一下)

不过现在的 Web 应用越来越复杂了,有些应用开始尝试在浏览器端做一些复杂的计算,例如图片的处理、或是机器学习模型的运算…等等。JavaScript 原本就不是设计用来做复杂运算的,如果你想透过 JavaScript 做一些繁杂的运算,即使背後有 Event Loop 机制,但因爲 single threaded 的特性,实际在运行时同时间还是只能做一件事,在计算完之前,可能会让页面卡住并失去响应,没办法做其他事情。

例如在 browser console 中跑一个很大的回圈并做一些事,页面马上就 crash 了,并且没有办法做任何操作。

的确我们应该尽量避免在 client side 做一些复杂的操作,因为我们分身乏术...

难道在浏览器上执行的 JavaScript 就只能接受这样悲惨的命运吗?Web 技术越来越进步的同时,大家都说浏览器未来可以做到更多事,如果被受限不能做太复杂的运算实在有点可惜,有没有办法可以解开 single threaded 分身乏术的限制呢?在 Node.js 环境可以使用 cluster module 或是 child_process module 来解决,那在浏览器端有没有什麽解决方案呢?

其实还是有办法可以成功在浏览器使出影分身之术的!Let's welcome Web Workers.

什麽是 Web Workers ?

Web Workers 是浏览器提供的一个 Web API,不过它其实已经存在一段时间了,目前主流的浏览器几乎都已经支援。

正常状况下在浏览器上的 JavaScript 都是在 Main Thread (又称 UI Thread) 执行的,然而 Main Thread 要做的事实在是太多了,例如页面的渲染、解析与执行程序码、计算样式、画出页面中的每一个像素等工作都要依赖 Main Thread 来执行,有了 Web Worker,我们可以开出一条新的 thread 来执行 JavaScript,两条 thread 不会互相影响,并且可以透过 onMessage、postMessage 等 API 做讯息的沟通,达到平行运算的能力。

Web Workers 的限制

哇!看起来很猛欸,可以做到真正的平行执行,那我不就把 Web Worker 开好开满效能就会提升了吗!?太天真罗!孩子,Web Worker 是有许多限制存在的,例如:

  • 不能存取 DOM, document, window, parent 等资源
  • 主页面与 Worker 的资源不能共享
  • Web Workers 采用 share-nothing 模型,必须使用 postMessage 和 onMessage 等 API 来沟通
  • 同源限制:Worker 使用的 script 不能是来自其他网站的
  • 虽然说规范上没有限制最多可以创建几个 worker,不过每开一个 worker 都需要消耗 CPU 与 Memory,所以不能滥用,不然有可能导致效能反而更糟的状况。再者与 worker 沟通也是有成本的,例如资料的拷贝与解析也是需要时间的(下图的橘色区块),实际上能不能达到 parallel 也是要看 device 的硬体资源来决定,这些都是需要考虑进去的问题。


(橘色区块为与 workers 沟通的成本)

有这些限制也是可以理解的,毕竟 Web Workers 是会产生真正 OS 层级的执行绪,thread safe 就成为必须考量的因素,禁止使用 DOM 等物件也是为了避免 multi threading 容易产生的 race condition。

等等…这样看起来它好像突然变成什麽都不能做了?也没有到那麽糟啦,它仍然可以做一些用一些常见的 Web API,例如:

  • XMLHttpRequest
  • setTimeout & setInterval

简单来说 Web Workers 可以使用基本的 JavaScript 功能与一些特定的 Web API,想要更近一步知道 Web Worker 到底可以使用哪些 function 的人可以看 MDN 的文件

Web Workers 的基本使用方式

虽然说在网路上查 Web Workers 的使用方式应该可以得到许多类似的范例,但既然这个系列文的特色是要尽可能详细的介绍各种优化技巧,那我当然不能不负责任的跳过,还是得简单介绍 Web Workers 最基本的使用方式。接下来将简单示范如何把费波那契数列的计算丢到 Web Worker 处理,虽然了无新意,但的确能让第一次接触的读者快速了解 Web Worker 的使用方式。

这个使用 Web Workers 计算费波那契数列的范例中会分为两个档案:

  • 主程序
  • Worker 程序

首先是 worker 的程序码

// worker.js
const fibonacci = (n) => {  
  if(n <= 0) { 
    return 0; 
  } 
  if(n === 1) { 
    return 1 
  }
  return fibonacci(n - 1) + fibonacci(n - 2); 
}
// event listener
// 在 worker 程序中 self 指的是 worker 本身的 instance
self.onmessage = e => {
  const number = e.data;  
  const result = fibonacci(number);
  self.postMessage(result); 
};

再来是主程序的程序码

// 检查浏览器有没有支援 Web Workers
if(window.Worker) {
  // 建立 worker instance
  const worker = new Worker("./worker.js");
  worker.onmessage = e => {   
    const result = e.data;   
    console.log('Result is...', result);
  };   
  worker.onerror = e => {   
    console.log('error');
  };
  document.querySelector('#calculate').onclick = function() {
     console.log('send number to web workers');   
     const number = document.querySelector('#number-input').value;
     worker.postMessage(number);
  }
}

基本上就是把要做的复杂计算写在 worker 的程序中,再透过 postMessage 的方式与主程序做资料沟通,在这之前记得要在主程序创建 Worker 的 instance。上面的范例中我们在 id 为 calculate 的 element 被点击时会将一个输入匡的数值当作参数传到 Web Workers 中做费氏数列的计算,计算完後再透过 postMessage 将结果传回主程序,接下来就可以根据需求看要更新 DOM 的 text 或是进行更进阶的操作罗。

(测试一下应该会发现数列的计算因为是在另外的 thread 执行,所以不会卡到页面的渲染流程)

资料的所有权转移

刚刚有提到 Web Workers 要与其他程序通讯时可以透过 postMessage 的方式,可以传送的资料包含 JS 的 Object 还有 Blob、ArrayBuffer 等 Binary 的资料,不过这些资料都会透过 Deep Copy 的方式传送,要注意有时候 Deep Copy 会造成效能的瓶颈, 我们可以透过转移 ownership 的方式来减缓效能的耗损

const uInt8Array = new Uint8Array(1024 * 1024 * 32); // 32MB
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i;
}

worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

如果是来自 C/C++ 背景的读者可能会觉得有点类似 pass by reference,不同的是在转换 ownership 後原本的资料就不能再使用了。以上面的例子来说在主程序宣告的 ArrayBuffer 如果将 ownership 转移给 worker 程序後就没有办法使用它了,只有收到 ownership 的 worker 程序可以接着存取它。而有实作 Transferable 这个 interface 的物件才有办法做所有权的转移,这些物件也被称作 Transferable 的物件,常见的有:ArrayBuffer、MessagePort、ImageBitmap, OffScreenCanvas。

平行化程序设计

到目前为止 Worker 的资料都是独立的,不管是透过 Deep Copy 或是转移 Ownership 的方式,资料都只能在单一 worker 内共用,没办法共享。不过有时候可能会有一些特殊的平行化程序设计的需求,这时候资料的共享就成了不可或缺的特性,於是一些特别的资料结构诞生了,例如 SharedArrayBuffer

为了避免 multi-thread 造成 race condition,可能还得搭配 Atomics 这个 API 来开发。因为属於比较进阶的主题,就不在本篇说明罗,有兴趣的读者可以根据连结去深入研究!

(要注意这些相关的 Web API 的浏览器支援度目前都还不是很好,SharedArrayBuffer 甚至因为安全性问题被各大浏览器禁用,一些浏览器例如 Chrome 则需要透过特殊设定才能开启 SharedArrayBuffer)

Web Workers 的使用场景

其实我觉得这才是本篇的重点所在,要知道 Web Workers 的使用方式其实照着文件做就行了,没有什麽困难的地方,而 Web Workers 到底可以做些什麽?适合哪些应用场景我认为是更为重要的概念。

网路上许多资源在介绍 Web Workers 时都是使用像刚刚的费氏数列或是回圈等耗时但一般在开发时几乎不会遇到的状况,这增加了读者的疑惑,到底哪些操作适合送到 Web Workers 去做呢?关於应用场景,我的建议如下:

  • 真的需要高度计算的任务,例如复杂的排序与 index
  • Machine Learning、加解密
  • 图片的操作
  • 游戏开发
  • Web AR/VR
  • Long Polling
  • WebAssembly(明天会介绍)

总结来说因为开启 Web Workers 与执行绪间的通讯都要耗费资源,所以我们不可能把所有操作都丢到 Web Worker 去处理,乱用的下场可能网页效能会直接炸掉。一般来说如果操作是非常耗时耗工的,又跟 UI、渲染流程比较没有关系的任务,就有机会可以透过 Web Workers 的帮忙而提升效能,同时也能让 Main Thread 专注於 UI flow 相关的任务,除了使效能与使用者体验提升外,也达到了关注点的分离。

Comlink : Use Web Workers Easier

其实前面介绍的 Web Workers 的使用方式并不是那麽直觉,试想一下在使用前端框架例如 React 建造的专案中写一堆 postMessage 的程序码,想到就觉得有点不舒服,程序码的可维护性也因此降低了。因此蛮推荐读者使用一个由 Google 开发,让使用 Web Workers 更为方便且抽象的套件:Comlink 来开发 Web Workers 相关的程序码。

Comlink 非常的轻量化(1.1KB),是针对 Web Workers 的一层抽象封装 (Comlink 的 source code 也非常的少,有兴趣的读者可以看这里)。Comlink 可以让开发者「在使用 Web Workers 时感觉不到自己正在使用 Web Workers。」听起来很玄,其实就是做了一层封装,让开发者可以用像是操作类别与物件的方式来操作 Web Workers,而不用写一堆 Web Workers 独有的 postMessage API,例如下图是 Comlink 官方提供的范例图片:

可以发现使用方式变得直觉许多。

本日小结

Main Thread 又被称作 UI Thread,但有时候它包揽了太多 UI 以外的事,例如复杂的计算等等,受限於 JS 的 single-threaded 特性,有时候复杂的运算会对页面的渲染等流程造成负面的影响。

有了 Web Worker 的出现,总算有办法在浏览器使出影分身之术,在不同於 Main Thread 的执行绪中平行执行复杂的运算。虽然它的限制还蛮多的,例如无法存取 DOM 物件,与 Main Thread 沟通也需要付出一定的成本,因此不能滥用,否则对效能反而会带来负面的影响,又或许在一般的 Web App 中也很少看到它的身影。不过随着浏览器的进步,越来越多的任务有机会在浏览器端完成,让 Web Workers 也多了更多可以发光发热的机会,例如明天要介绍的 WebAssembly 或是近期飞快发展的在浏览器端的机器学习都是很好的例子,也有 Google 的工程师大大认为可以把与 UI 无关的商业逻辑抽到 Web Workers 处理。虽然 Web Workers 存在已久却好像没有被普遍的应用,让一些人对它失去了信心,但我个人是相信未来会有很多很多应用需要依赖 Web Workers 的帮忙的,Let's wait and see !

References & 图片来源

https://medium.com/r/?url=https%3A%2F%2Fithelp.ithome.com.tw%2Farticles%2F10118851

https://medium.com/r/?url=https%3A%2F%2Fblog.logrocket.com%2Fcomlink-web-workers-match-made-in-heaven%2F

https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2FGoogleChromeLabs%2Fcomlink

https://medium.com/r/?url=https%3A%2F%2Ftigercosmos.xyz%2Fpost%2F2020%2F02%2Fweb%2Fjs-parallel-worker-sharedarraybuffer%2F

https://medium.com/r/?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FWeb_Workers_API%2FUsing_web_workers


<<:  永远不回头的离职档案管理

>>:  命令提示字元--CMD那麽好用你不学一下吗?

Day 8 超多的范例?怎麽办呢?

该文章同步发布於:我的部落格 昨天我们做了一个关於汉堡种类的测试,但真正的测试怎麽可能这麽少呢! ...

电子书阅读器上的浏览器 [Day09] 支援音量键翻页

虽然 browser 下方的工具列已经添加了上下按钮,可以在不卷动画面的情况下,往下一页或往上一页,...

[Day 11] Sass - Operators

Operators 今天要来介绍一下Sass的Oerators-运算功能 虽然在一般的CSS中,我们...

Day027-透过Vuex-实作简易部落格-列举及删除文章

Vue:昨日,我们已将文章新增实做出来了!现在只要将文章列举在首页,只需要使用之前学到的v-for回...

【Day17】期间限定:函式的参数

函式会将参数传入函式里面,让它们成为函式里的变数,让程序码去做运算。参数只能在函式里刷存在感(期间...