非主執行緒架構可大幅提升應用程式的可靠性和使用者體驗。
在過去 20 年間,網路從只有幾種樣式和圖片的靜態文件,演變為複雜的動態應用程式。不過,有一個部分幾乎沒有改變:每個瀏覽器分頁 (有些例外情況) 只有一個執行緒,用於轉譯網站和執行 JavaScript。
因此,主執行緒的負擔變得非常沉重。隨著網頁應用程式變得越來越複雜,主要執行緒就會成為效能的重要瓶頸。更糟的是,在主執行緒上為特定使用者執行程式碼所需的時間幾乎完全無法預測,因為裝置功能會對效能造成巨大影響。隨著使用者透過越來越多樣化的裝置存取網際網路,從功能極為受限的功能型手機,到效能強大、刷新率高的旗艦機種,這種不可預測性只會越來越嚴重。
如果我們希望複雜的網頁應用程式可穩定地符合效能指南 (例如根據人類感知和心理學的經驗資料所制定的 Core Web Vitals),就需要找到方法,在主執行緒 (OMT) 之外執行程式碼。
為什麼要使用網頁工作者?
根據預設,JavaScript 是單執行緒語言,會在主執行緒上執行工作。不過,Web Workers 可讓開發人員建立獨立的執行緒,以便處理主執行緒以外的工作,因此可從主執行緒中脫離。雖然網頁工作者的範圍有限,且無法直接存取 DOM,但如果需要執行大量工作而主執行緒無法負荷,網頁工作者就會非常有用。
在Core Web Vitals 方面,從主執行緒中移出工作可能會有所幫助。特別是,將工作從主執行緒卸載至網路 worker,可以減少主執行緒的競爭,進而改善頁面的 Interaction to Next Paint (INP) 回應速度指標。主執行緒的工作量較少時,就能更快速地回應使用者互動。
減少主執行緒的工作量 (尤其是在啟動期間),也能減少長時間的工作,進而提升最大內容繪製 (LCP) 的效益。算繪 LCP 元素需要主執行緒時間,無論是算繪文字或圖片 (這兩者都是常見的 LCP 元素),而且透過減少整體主執行緒工作,您就能確保網頁的 LCP 元素不太可能遭到耗時工作阻斷,而這些耗時工作可由網頁工作項處理。
使用網路 worker 建立執行緒
其他平台通常會透過允許您為執行緒提供函式,以便與其他程式並行執行,來支援平行工作。您可以從兩個執行緒存取相同的變數,而且這些共用資源的存取權可與互斥鎖和訊號量同步,以防競爭狀況發生。
在 JavaScript 中,我們可以透過網路工作者取得大致類似的功能,這些功能自 2007 年推出以來,自 2012 年起已獲得所有主要瀏覽器的支援。Web worker 會與主執行緒並行執行,但與作業系統執行緒不同,Web worker 無法共用變數。
如要建立網頁工作站,請將檔案傳遞至 worker 建構函式,以便在個別執行緒中開始執行該檔案:
const worker = new Worker("./worker.js");
使用 postMessage
API 傳送訊息,與網路工作者進行通訊。在 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
// ...
});
如要將訊息傳回至主執行緒,請在網路 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);
});
不過,這種方法的功能有限。以往,網頁工作站主要用於將單一繁重工作移出主執行緒。嘗試使用單一網頁工作者處理多項作業很快就會變得難以管理:您不僅要對訊息中的參數進行編碼,還要對作業進行編碼,並且必須進行記帳,才能將回應與要求配對。這項複雜性可能是導致網頁工作站未廣泛採用的原因。
不過,如果我們能解決主執行緒和網頁工作者之間通訊的部分困難,這個模型就非常適合許多用途。幸運的是,有一個程式庫可以完成這項工作!
Comlink:減少網頁工作站的工作量
Comlink 是可讓您使用網路 worker 的程式庫,無須考慮 postMessage
的詳細資料。透過 Comlink,您可以在網路 worker 和主執行緒之間共用變數,這與支援執行緒的其他程式設計語言類似。
您可以透過在網路工作站中匯入 Comlink,並定義一組要公開至主執行緒的函式,來設定 Comlink。接著,您可以在主執行緒上匯入 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
變數與網頁工作者中的變數行為相同,唯一差別是每個函式會傳回值的承諾,而非值本身。
您應該將哪些程式碼移至網頁工作站?
網路 worker 無法存取 DOM 和許多 API (例如 WebUSB、WebRTC 或 Web Audio),因此您無法將依賴這類存取權的應用程式部分放入 worker。不過,將每個小程式碼移至 worker 後,主要執行緒就能為必須在該執行緒中執行的作業,例如更新使用者介面,提供更多空間。
網頁開發人員面臨的問題是,大多數網頁應用程式都會依賴 Vue 或 React 等 UI 架構,用於協調應用程式中的所有內容;所有內容都是架構的元件,因此與 DOM 有內在關聯。這似乎會導致難以遷移至 OMT 架構。
不過,如果我們改用將 UI 問題與其他問題 (例如狀態管理) 分開的模型,即使是採用架構的應用程式,網頁工作緒也能發揮很大的效用。這正是 PROXX 採用的方法。
PROXX:OMT 個案研究
Google Chrome 團隊開發的 PROXX 是符合漸進式網頁應用程式規定的「地雷尋寶」克隆版,包括可離線運作及提供吸引人的使用者體驗。不過,早期版本的遊戲在功能手機等受限裝置上表現不佳,因此團隊發現主要執行緒是瓶頸。
團隊決定使用 Web Workers,將遊戲的視覺狀態與邏輯分開:
- 主執行緒會處理動畫和轉場效果的算繪作業。
- Web worker 會處理純粹運算的遊戲邏輯。
OMT 對 PROXX 的功能型手機效能有趣的影響。在非 OMT 版本中,使用者與 UI 互動後,UI 會凍結六秒。系統不會提供任何回饋,使用者必須等待完整的六秒鐘,才能執行其他操作。
不過,在 OMT 版本中,遊戲需要 十二 秒才能完成 UI 更新。雖然這似乎會導致效能下降,但實際上會讓使用者獲得更多意見回饋。發生速度變慢的情形,是因為應用程式傳送的畫面數量比非 OMT 版本多,後者根本沒有傳送任何畫面。因此,使用者會知道發生了什麼事,並可在 UI 更新時繼續玩遊戲,讓遊戲體驗大幅提升。
這是有意做出的取捨:我們為受限裝置的使用者提供「更佳」的體驗,同時不會對高階裝置的使用者造成懲罰。
OMT 架構的影響
如 PROXX 範例所示,OMT 可讓應用程式在更多裝置上穩定執行,但不會加快應用程式的速度:
- 您只是將工作從主執行緒移出,並未減少工作量。
- 網路輔助程式和主執行緒之間的額外通訊開銷有時可能會讓速度略微下降。
考量取捨
由於主執行緒可在 JavaScript 執行期間自由處理使用者互動作業 (例如捲動畫面),因此即使總等待時間可能會略微延長,但遺漏的畫面會減少。讓使用者稍微等待一下,比丟棄影格更為理想,因為丟棄影格時的誤差範圍較小:丟棄影格只會發生在毫秒內,而使用者感知等待時間之前,有 數百 毫秒的時間。
由於不同裝置的效能無法預測,因此 OMT 架構的目標其實是降低風險,也就是讓應用程式在面對變化多端的執行階段條件時更為穩健,而非追求並行化的效能優勢。彈性提升和 UX 改善的效果,遠勝於速度的微小權衡。
工具相關注意事項
Web worker 尚未成為主流技術,因此大多數模組工具 (例如 webpack 和 Rollup) 並未預設支援這項技術。(不過 Parcel 會執行這項操作!)幸好,有外掛程式可讓網路工作站與 webpack 和 Rollup 搭配運作:
- webpack 適用的worker-plugin
- Rollup 的 rollup-plugin-off-main-thread
總結
為了確保應用程式盡可能可靠且易於存取,尤其是在日益全球化的市場中,我們需要支援受限裝置,因為全球大多數使用者都是透過這類裝置存取網際網路。OMT 提供可行的方法,可在不影響高階裝置使用者的情況下,提升這類裝置的效能。
此外,OMT 還有其他好處:
- 將 JavaScript 執行成本移至另一個執行緒。
- 這可降低剖析成本,也就是說 UI 可能會更快啟動。這樣做可能會縮短首次顯示內容所需時間,甚至互動時間,進而提高 Lighthouse 分數。
使用 Web worker 不必感到害怕。Comlink 等工具可協助員工完成工作,並為各種網路應用程式提供可行選擇。