非主執行緒架構可大幅提升應用程式的穩定性和使用者體驗。
過去 20 年來,網路已從只有少數樣式和圖片的靜態文件,大幅演變為複雜的動態應用程式。不過,有一點大致上維持不變:每個瀏覽器分頁只有一個執行緒 (少數例外),用於轉譯網站和執行 JavaScript。
因此,主執行緒的工作量變得非常大。隨著網頁應用程式越來越複雜,主執行緒會成為效能的重大瓶頸。更糟的是,在主執行緒上為特定使用者執行程式碼所需的時間幾乎完全無法預測,因為裝置功能對效能有極大影響。隨著使用者透過越來越多樣化的裝置存取網路,從功能受限的功能型手機到高效能、高刷新率的旗艦機,這種不可預測性只會越來越高。
如果我們希望複雜的網頁應用程式能可靠地符合 Core Web Vitals 等效能指南 (根據人類感知和心理學的經驗資料),就必須在主執行緒以外 (OMT) 執行程式碼。
為什麼要使用 Web Worker?
根據預設,JavaScript 是一種單一執行緒語言,會在主執行緒上執行工作。不過,Web Worker 可讓開發人員建立獨立執行緒,在主執行緒外處理工作,為主執行緒提供一種逃生艙。雖然 Web Worker 的範圍有限,且無法直接存取 DOM,但如果有大量工作需要完成,否則會導致主執行緒不堪負荷,Web Worker 就會非常實用。
就 Core Web Vitals 而言,在主執行緒外執行工作可能會有幫助。具體來說,將工作從主執行緒卸載至 Web Worker,可減少主執行緒的爭用情形,進而改善網頁的 Interaction to Next Paint (INP) 回應速度指標。主執行緒要處理的工作減少時,就能更快回應使用者互動。
減少主執行緒工作 (尤其是在啟動期間),也能減少長時間任務,進而提升最大內容繪製 (LCP)。算繪 LCP 元素需要主執行緒時間,無論是算繪文字或圖片 (常見的 LCP 元素),減少主執行緒工作整體,可確保網頁的 LCP 元素較不易受到耗費資源的工作阻礙,而這些工作可由 Web Worker 處理。
使用 Web Worker 執行緒
其他平台通常支援平行工作,可讓您為執行緒提供函式,與程式的其他部分平行執行。您可以從兩個執行緒存取相同的變數,並使用互斥鎖和信號同步存取這些共用資源,避免競爭狀況。
在 JavaScript 中,我們可以透過 Web Worker 取得大致類似的功能,Web Worker 自 2007 年推出,自 2012 年起所有主要瀏覽器都支援。Web worker 會與主執行緒平行執行,但與 OS 執行緒不同,Web worker 無法共用變數。
如要建立 Web Worker,請將檔案傳遞至 Worker 建構函式,該函式會在個別執行緒中開始執行該檔案:
const worker = new Worker("./worker.js");
使用 postMessage API 傳送訊息,與 Web Worker 通訊。在 postMessage 呼叫中將訊息值做為參數傳遞,然後在 worker 中新增訊息事件監聽器:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
// ...
});
如要將訊息傳回主執行緒,請在 Web Worker 中使用相同的 postMessage API,並在主執行緒上設定事件監聽器:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
console.log(event.data);
});
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
postMessage(a + b);
});
不過,這個方法有一些限制。以往,Web Worker 主要用於將單一繁重工作移出主執行緒。如果嘗試使用單一 Web Worker 處理多項作業,很快就會變得難以管理:您不僅要對參數進行編碼,還要對訊息中的作業進行編碼,而且必須進行記錄,以便將回應與要求進行比對。這項複雜性可能是 Web Worker 未廣泛採用的原因。
不過,如果能降低主執行緒和 Web Worker 之間的通訊難度,這個模型就能適用於許多用途。幸好,有一個程式庫可以做到這一點!
Comlink:減輕 Web Worker 的工作量
Comlink 程式庫可讓您使用 Web Worker,而不必考慮 postMessage 的細節。Comlink 可讓您在 Web Worker 和主執行緒之間共用變數,就像其他支援執行緒的程式設計語言一樣。
如要設定 Comlink,請將其匯入 Web Worker,並定義一組要向主執行緒公開的函式。接著,在主執行緒上匯入 Comlink、包裝 worker,並存取公開的函式:
worker.js
import {expose} from 'comlink';
const api = {
someMethod() {
// ...
}
}
expose(api);
main.js
import {wrap} from 'comlink';
const worker = new Worker('./worker.js');
const api = wrap(worker);
主執行緒上的 api 變數行為與 Web Worker 中的變數相同,但每個函式都會傳回值的 Promise,而非值本身。
哪些程式碼應移至 Web Worker?
Web worker 無法存取 DOM 和許多 API,例如 WebUSB、WebRTC 或 Web Audio,因此您無法將應用程式中依賴這類存取權的部分放入 worker。不過,每將一小段程式碼移至 worker,就能為主執行緒爭取更多空間,用於處理「必須」在主執行緒執行的工作,例如更新使用者介面。
網頁程式開發人員面臨的問題之一,是大多數網頁應用程式都仰賴 Vue 或 React 等 UI 架構來協調應用程式中的所有內容;所有內容都是架構的元件,因此本質上與 DOM 綁定。這似乎會讓遷移至 OMT 架構變得困難。
不過,如果我們改用將 UI 考量與其他考量 (例如狀態管理) 分開的模型,即使是採用框架的應用程式,Web Worker 也能發揮相當大的作用。這正是 PROXX 採用的方法。
PROXX:OMT 個案研究
Google Chrome 團隊開發了 PROXX,這是一款符合漸進式網頁應用程式需求的踩地雷複製遊戲,包括離線運作和提供吸引人的使用者體驗。可惜的是,遊戲早期版本在功能型手機等受限裝置上的效能不佳,因此團隊意識到主執行緒是瓶頸。
團隊決定使用 Web Worker,將遊戲的視覺狀態與邏輯分開:
- 主執行緒會處理動畫和轉場效果的算繪作業。
- Web worker 負責處理遊戲邏輯,這純粹是運算作業。
OMT 對 PROXX 的功能型手機效能產生了有趣的影響。在非 OMT 版本中,使用者與 UI 互動後,UI 會凍結六秒。系統不會提供任何意見回饋,使用者必須等待整整六秒,才能執行其他操作。
但在 OMT 版本中,遊戲需要 12 秒才能完成 UI 更新。雖然這似乎會降低效能,但實際上會增加對使用者的意見回饋。之所以會變慢,是因為應用程式傳送的影格比非 OMT 版本多,而非 OMT 版本完全不會傳送任何影格。因此,使用者知道有事情正在發生,且可以在 UI 更新時繼續玩遊戲,大幅提升遊戲體驗。
這是一種有意識的取捨:我們為受限裝置的使用者提供感覺更好的體驗,同時不會對高階裝置的使用者造成負面影響。
OMT 架構的影響
如 PROXX 範例所示,OMT 可讓應用程式在更多裝置上穩定執行,但不會加快應用程式速度:
- 您只是將工作從主執行緒移出,並未減少工作量。
- Web Worker 和主執行緒之間的額外通訊負擔,有時可能會稍微降低速度。
權衡利弊
由於主執行緒在 JavaScript 執行時,可以自由處理捲動等使用者互動,因此即使總等待時間可能略長,影格遺失的情況也會減少。讓使用者稍等片刻,會比捨棄影格更好,因為捨棄影格的容錯空間較小:捨棄影格會在幾毫秒內發生,而使用者需要數百毫秒才會察覺到等待時間。
由於跨裝置效能無法預測,OMT 架構的目標實際上是降低風險,讓應用程式在面對高度變動的執行階段條件時更穩健,而不是平行處理的效能優勢。雖然速度會稍微降低,但能提升韌性和使用者體驗,因此非常值得。
工具注意事項
Web worker 尚未成為主流,因此大多數模組工具 (例如 webpack 和 Rollup) 都不支援。(Parcel 則會)。幸好,有些外掛程式可以讓 Web Worker 與 webpack 和 Rollup 搭配運作:
- webpack 的 worker-plugin
- Rollup 的 rollup-plugin-off-main-thread
總結
為了確保應用程式盡可能可靠且易於存取,尤其是在日益全球化的市場中,我們需要支援受限裝置,因為全球大多數使用者都是透過這類裝置存取網路。OMT 是一種很有前景的方法,可提高這類裝置的效能,而不會對高階裝置的使用者造成負面影響。
此外,OMT 還有次要優點:
- 將 JavaScript 執行成本移至另一個執行緒。
- 這會轉移剖析成本,因此 UI 可能會更快啟動。這可能會縮短First Contentful Paint,甚至Time to Interactive,進而提高 Lighthouse 分數。
Web worker 並不可怕。Comlink 等工具可簡化 worker 的工作,讓 worker 成為各種網頁應用程式的可行選擇。