iT邦幫忙

2021 iThome 鐵人賽

DAY 22
2

身為前端開發者,整日與 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


上一篇
Day21 X Upgrade Your HTTP Connection
下一篇
Day23 X WebAssembly
系列文
今晚,我想來點 Web 前端效能優化大補帖!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
SuoChan 阿關
iT邦新手 2 級 ‧ 2021-10-07 01:07:13

Kyle 大大~
今天好像是第 22 天 XD

已修正 感謝XDD 寫到昏頭了

0
WeiYuan
iT邦新手 4 級 ‧ 2021-10-07 01:10:24

為什麼你每天都可以寫那麼多字(怕

用生命在寫鐵人賽...XDD

我要留言

立即登入留言