Web Worker 文獻綜述

blank

Web Worker 文獻綜述

Web Worker作為瀏覽器多線程技術,在頁面內容不斷豐富,功能日趨複雜的當下,成為緩解頁面卡頓,提升應用性能的可選方案.
但她的容顏, 隱藏在邊緣試探的科普文章和不知深淺的兼容性背後; 對JS 單線程面試題倒背如流的前端工程師, 對多線程開發有著天然的陌生感.

原文地址:

背景

文獻綜述

文献综述(Literature Review)是學術研究領域一個常見概念,寫過畢業論文的同學應該還有印象.它向讀者介紹與主題有關的詳細資料、動態、進展、展望以及對以上方面的評述.

近期筆者關注Web Worker,並落地到了大型複雜前端項目.開源了Worker通信框架alloy-worker ,正在寫實踐總結文章.其間查閱了相關資料(50+文章, 10+技術演講),獨立寫成這篇綜述性文章.

主要內容

  • Worker 發展歷史
  • 主線程和多線程
  • Worker 應用場景
  • 語法和運行環境
  • Worker 通信速度
  • 瀏覽器兼容性
  • 調試工具用法
  • 社區配套工具
  • 業界實踐回顧
  • 實踐建議和總結

發展歷史

簡介

前端同學對Web Worker應該不陌生,即使沒有動手實踐過,應該也在社區上看過相關文章.在介紹和使用上,官方文檔是MDN的Web Workers API .其對Web Worker的表述是:

Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application.

如下圖所示, Web Worker實現了多線程運行JS能力.之前頁面更新要先串行(Serial)做2件事情;使用Worker後, 2件事情可並行(Parallel)完成.

blank

圖片來源

可以直觀地聯想:並行可能會提升執行效率;運行任務拆分能減少頁面卡頓.後面應用場景章節將繼續討論.

技術規範

Web Worker屬於HTML規範,規範文檔見Web Workers Working Draft ,有興趣的同學可以讀一讀.而它並不是很新的技術,如下圖所示: 2009年就提出了草案.

blank

圖片來源

同年在FireFox3.5上率先實現,可以在using web workers: working smarter, not harder中看到早期的實踐. 2012年發布的IE10也實現了Web Worker,標誌著主流瀏覽器上的全面支持. IE10的Web Worker 能力測試如下圖所示:

圖片來源

在預研Worker方案時,開發人員會有兼容性顧慮.這種顧慮的普遍存在,主要由於業界Worker技術實踐較少和社區推廣不活躍.單從發展歷史看, Worker從2012年起就廣泛可用;後面兼容性章節將繼續討論.

DedicatedWorker 和SharedWorker

Web Worker規範中包括: DedicatedWorkerSharedWorker ;規範並不包括Service Worker,本文也不會展開討論.

blank

圖片來源

如上圖所示, DedicatedWorker簡稱Worker,其線程只能與一個頁面渲染進程(Render Process)進行綁定和通信,不能多Tab共享. DedicatedWorker是最早實現並最廣泛支持的Web Worker能力.

而SharedWorker可以在多個瀏覽器Tab中訪問到同一個Worker實例,實現多Tab共享數據,共享webSocket連接等.看起來很美好,但safari放棄了SharedWorker支持,因為webkit引擎的技術原因.如下圖所示, 只在safari 5~6 中短暫支持過.

blank

圖片來源

社區也在討論是否繼續支持SharedWorker ;多Tab共享資源的需求建議在Service Worker上尋找方案.

相比之下, DedicatedWorker 有著更廣的兼容性和更多業務落地實踐, 本文後面討論中的Worker 都是特指DedicatedWorker.

主線程和多線程

用戶使用瀏覽器一般會打開多個頁面(多Tab),現代瀏覽器使用單獨的進程(Render Process)渲染每個頁面,以提升頁面性能和穩定性,並進行操作系統級別的內存隔離.

blank

圖片來源

主線程(Main Thread)

頁面內,內容渲染和用戶交互主要由Render Process中的主線程進行管理.主線程渲染頁面每一幀(Frame),如下圖所示,會包含5個步驟: JavaScript → Style → Layout → Paint → Composite , 如果JS 的執行修改了DOM, 可能還會暫停JS, 插入並執行Style 和Layout.

blank

圖片來源

而我們熟知的JS單線程和Event Loop ,是主線程的一部分. JS單線程執行避免了多線程開發中的複雜場景(如競態和死鎖).但單線程的主要困擾是:主線程同步JS執行耗時過久時(瀏覽器理想幀間隔約16ms),會阻塞用戶交互和頁面渲染.

blank

圖片來源

如上圖所示, 長耗時任務執行時, 頁面將無法更新, 也無法響應用戶的輸入/點擊/滾動等操作. 如果卡死太久, 瀏覽器可能會拋出卡頓的提示. 如下圖所示.

  • Chrome81
blank
  • IE11
blank

多線程

Web Worker會創建操作系統級別的線程.

The Worker interface spawns real OS-level threads . -- MDN

JS 多線程, 是有獨立於主線程的JS 運行環境. 如下圖所示: Worker 線程有獨立的內存空間, Message Queue, Event Loop, Call Stack 等, 線程間通過postMessage 通信.

blank

多個線程可以並發運行JS.熟悉JS異步編程的同學可能會說, setTimeout / Promise.all不就是並發嗎,我寫得可溜了.

JS單線程中的"並發",準確來說是Concurrent .如下圖所示,運行時只有一個函數調用棧,通過Event Loop實現不同Task的上下文切換(Context Switch).這些Task通過BOM API調起其他線程為主線程工作,但回調函數代碼邏輯依然由JS串行運行.

Web Worker是JS多線程運行技術,準確來說是Parallel .其與Concurrent的區別如下圖所示: Parallel有多個函數調用棧,每個函數調用棧可以獨立運行Task,互不干擾.

blank

應用場景

討論完主線程和多線程, 我們能更好地理解Worker 多線程的應用場景:

  • 可以減少主線程卡頓.
  • 可能會帶來性能提升.

減少卡頓

根據Chrome團隊提出的用戶感知性能模型RAIL ,同步JS執行時間不能過長.量化來說,播放動畫時建議小於16ms,用戶操作響應建議小於100ms,頁面打開到開始呈現內容建議小於1000ms.

邏輯異步化

減少主線程卡頓的主要方法為異步化執行,比如播放動畫時,將同步任務拆分為多個小於16ms的子任務,然後在頁面每一幀前通過requestAnimationFrame按計劃執行一個子任務,直到全部子任務執行完畢.

blank

圖片來源

拆分同步邏輯的異步方案對大部分場景有效果, 但並不是一勞永逸的銀彈. 有以下幾個問題:

  • 不是所有JS邏輯都可拆分.比如數組排序,樹的遞歸查找,圖像處理算法等,執行中需要維護當前狀態,且調用上非線性,無法輕易地拆分為子任務.
  • 可以拆分的邏輯難以把控粒度.如下圖所示,拆分的子任務在高性能機器(iphoneX)上可以控制在16ms內,但在性能落後機器(iphone6)上就超過了deadline. 16ms的用戶感知時間,並不會因為用戶手上機器的差別而變化, Google給出的建議是再拆小到3-4ms .
blank

圖片來源

  • 拆分的子任務並不穩定.對同步JS邏輯的拆分,需要根據業務場景尋找原子邏輯,而原子邏輯會跟隨業務變化,每次改動業務都需要去review原子邏輯.

Worker 一步到位

Worker的多線程能力,使得同步JS任務的拆分一步到位:從宏觀上將整個同步JS任務異步化.不需要再去苦苦尋找原子邏輯,邏輯異步化的設計上也更加簡單和可維護.

這給我們帶來更多的想像空間. 如下圖所示, 在瀏覽器主線程渲染週期內, 將可能阻塞頁面渲染的JS 運行任務(Jank Job)遷移到Worker 線程中, 進而減少主線程的負擔, 縮短渲染間隔, 減少頁面卡頓.

blank

性能提升

Worker 多線程並不會直接帶來計算性能的提升, 能否提升與設備CPU 核數和線程策略有關.

多線程與CPU 核數

CPU 的單核(Single Core)和多核(Multi Core)離前端似乎有點遠了. 但在頁面上運用多線程技術時, 核數會影響線程創建策略.

進程是操作系統資源分配的基本單位,線程是操作系統調度CPU的基本單位.操作系統對線程能佔用的CPU計算資源有復雜的分配策略.如下圖所示:

  • 單核多線程通過時間切片交替執行.
  • 多核多線程可在不同核中真正並行.
blank

Worker 線程策略

一台設備上相同任務在各線程中運行耗時是一樣的. 如下圖所示: 我們將主線程JS 任務交給新建的Worker 線程, 任務在Worker 線程上運行並不會比原本主線程更快, 而線程新建消耗和通信開銷使得渲染間隔可能變得更久.

blank

圖片來源

在單核機器上,計算資源是內捲的,新建的Worker線程並不能為頁面爭取到更多的計算資源.在多核機器上,新建的Worker線程和主線程都能做運算,頁面總計算資源增多,但對單次任務來說,在哪個線程上運行耗時是一樣的.

真正帶來性能提升的是多核多線程並發.

如多個沒有依賴關係的同步任務,在單線程上只能串行執行,在多核多線程中可以並行執行.如下圖alloy-worker圖像處理demo所示,在iMac上運行時創建了6條Worker 線程, 圖像處理總時間比主線程串行處理快了約2000ms.

blank

值得注意的是,目前移動設備的核心數有限.最新iPhone Max Pro上搭載的A13芯片號稱6核,也只有2個高性能核芯(2.61G),另外4個是低頻率的能效核心(0.58 G). 所以在創建多條Worker 線程時, 建議區分場景和設備.

把主線程還給UI

Worker 的應用場景, 本質上是從主線程中剝離邏輯, 讓主線程專注於UI 渲染. 這種架構設計並非Web 技術上的獨創.

Android和iOS的原生開發中, 主線程負責UI工作;前端領域熱門的小程序,實現原理上就是渲染和邏輯的完全分離.

本該如此.

Worker API

通信API

blank

如上圖所示的Worker通信流程, Worker 通信API非常簡單.通俗中文教程可以參考Web Worker使用教程.使用細節建議看官方文檔.

雙向通信範例代碼如下圖所示,雙向通信只需7行代碼.

blank

主要流程為:

  1. 主線程調用new Worker(url)創建Worker實例, url為Worker JS資源url.
  2. 主線程調用postMessage發送hello ,在onmesssage中監聽Worker線程消息.
  3. Worker線程在onmessage中監聽主線程消息,收到主線程的hello ;通過postMessage回复world .
  4. 主線程在消息回調中收到Worker的world訊息.

postMessage會在接收線程創建一個MessageEvent ,傳遞的數據添加到event.data ,再觸發該事件; MessageEvent的回調函數進入Message Queue,成為待執行的宏任務.因此postMessage順序發送的訊息,在接收線程中會順序執行回調函數.而且我們無需擔心實例化Worker過程中postMessage的訊息丟失問題,對此Worker內部機制已經處理.

Worker 事件驅動(postMessage/onmessage) 的通信API 雖然簡潔, 但大多數場景下通信需要等待響應(類似HTTP 請求的Request 和Response), 並且多次同類型通信要匹配到各自的響應. 所以業務使用一般會封裝原生API,如封裝為Promise調用.這也是筆者開發alloy-worker的原由之一.

運行環境

在Worker線程中運行JS,會創建獨立於主線程的JS運行環境,稱之為DedicatedWorkerGlobalScope .開發者需關注Worker環境和主線程環境的異同,以及Worker在不同瀏覽器上的差異.

Worker 環境和主線程環境的異同

Worker是無UI的線程,無法調用UI相關的DOM/BOM API. Worker具體支持的API可參考MDN的functions and classes available to workers .

blank

圖片來源

上圖展示了Worker 線程與主線程的異同點. Worker 運行環境與主線程的共同點主要包括:

  • 包含完整的JS 運行時, 支持ECMAScript 規範定義的語言語法和內置對象.
  • 支持XmlHttpRequest ,能獨立發送網絡請求與後台交互.
  • 包含只讀的Location ,指向Worker線程執行的script url,可通過url傳遞參數給Worker環境.
  • 包含只讀的Navigator ,用於獲取瀏覽器訊息,如通過Navigator.userAgent識別瀏覽器.
  • 支持setTimeout / setInterval 計時器, 可用於實現異步邏輯.
  • 支持WebSocket 進行網絡I/O; 支持IndexedDB 進行文件I/O.

從共同點上看, Worker 線程其實很強大, 除了利用獨立線程執行重度邏輯外, 其網絡I/O 和文件I/O 能力給業務和技術方案帶來很大的想像空間.

blank

另一方面, Worker 線程運行環境和主線程的差異點有:

  • Worker 線程沒有DOM API, 無法新建和操作DOM; 也無法訪問到主線程的DOM Element.
  • Worker 線程和主線程間內存獨立, Worker 線程無法訪問頁面上的全局變量(window, document 等)和JS 函數.
  • Worker 線程不能調用alert() 或confirm() 等UI 相關的BOM API.
  • Worker 線程被主線程控制, 主線程可以新建和銷毀Worker.
  • Worker線程可以通過self.close自行銷毀.

從差異點上看, Worker 線程無法染指UI, 並受主線程控制, 適合默默幹活.

Worker 在不同瀏覽器上的差異

各家瀏覽器實現Worker 規範有差異, 對比主線程, 部分API 功能不完備, 如:

  • IE10 發送的AJAX 請求沒有referer, 請求可能被後台服務器拒絕.
  • Edge18 上字符編碼/ Buffer 的實現有問題.

好在這種場景並不多. 並且可以在運行時通過錯誤監控發現問題, 並定位和修復(polyfill).

另一方面, 一些新增的HTML 規範API 只在較新的瀏覽器上實現, Worker 運行環境甚至主線程上沒有, 使用Worker 時需判斷和兼容.

多線程同構代碼

Worker 線程不支持DOM, 這點和Node.js 非常像. 我們在Node.js 上做前後端同構的SSR 時, 經常會遇到調用BOM/DOM API 導致的報錯. 如下圖所示:

blank

在開發Worker 前端項目或遷移已有業務代碼到Worker 中時, 同構代碼比例可能很高, 容易調到BOM/DOM API. 可以通過構建變量區分代碼邏輯, 或運行時動態判斷所在線程, 實現同構代碼在不同線程環境下運行.

通信速度

Worker多線程雖然實現了JS任務的並行運行,也帶來額外的通信開銷.如下圖所示,從線程A調用postMessage發送數據到線程B onmessage接收到數據有時間差,這段時間差稱為通信消耗.

blank

圖片來源

提升的性能= 并行提升的性能– 通信消耗的性能.在線程計算能力固定的情況下,要通過多線程提升更多性能,需要盡量減少通信消耗.

而且主線程postMessage會佔用主線程同步執行,佔用時間與數據傳輸方式和數據規模相關.要避免多線程通信導致的主線程卡頓,需選擇合適的傳輸方式,並控制每個渲染週期內的數據傳輸規模.

數據傳輸方式

我們先來聊聊主線程和Worker 線程的數據傳輸方式. 根據計算機進程模型, 主線程和Worker 線程屬於同一進程, 可以訪問和操作進程的內存空間. 但為了降低多線程並發的邏輯複雜度, 部分傳輸方式直接隔離了線程間的內存, 相當於默認加了鎖.

通信方式有3 種: Structured Clone, Transfer Memory 和Shared Array Buffer.

Structured Clone

Structured Clone是postMessage默認的通信方式.如下圖所示,複製一份線程A的JS Object內存給到線程B,線程B能獲取和操作新復制的內存.

blank

Structured Clone通過複製內存的方式簡單有效地隔離不同線程內存,避免衝突;且傳輸的Object數據結構很靈活.但複製過程中,線程A要同步執行Object Serialization,線程B要同步執行Object Deserialization;如果Object規模過大, 會佔用大量的線程時間.

Transfer Memory

Transfer Memory意為轉移內存,它不需要Serialization/Deserialization,能大大減少傳輸過程佔用的線程時間.如下圖所示,線程A將指定內存的所有權和操作權轉給線程B,但轉讓後線程A無法再訪問這塊內存.

blank

Transfer Memory以失去控制權來換取高效傳輸,通過內存獨占給多線程並發加鎖.但只能轉讓ArrayBuffer等大小規整的二進制(Raw Binary)數據;對矩陣數據(如RGB圖片)比較適用.實踐上也要考慮從JS Object 生成二進制數據的運算成本.

Shared Array Buffers

Shared Array Buffer是共享內存,線程A和線程B可以同時訪問和操作同一塊內存空間.數據都共享了,也就沒有傳輸什麼事了.

blank

但多個並行的線程共享內存,會產生競爭問題(Race Conditions).不像前2種傳輸方式默認加鎖, Shared Array Buffers把難題拋給開發者,開發者可以用Atomics來維護這塊共享的內存.作為較新的傳輸方式,瀏覽器兼容性可想而知,目前只有Chrome 68+支持.

傳輸方式小結

  • 全瀏覽器兼容的Structured Clone是較好的選擇,但要考慮數據傳輸規模,下文我們會詳細展開.
  • Transfer Memory 的兼容性也不錯(IE11+), 但數據獨占和數據類型的限制, 使得它是特定場景的最優解, 不是通用解;
  • Shared Array Buffers 當下糟糕的兼容性和線程鎖的開發成本, 建議先暗中觀察.

JSON.stringify 更快?

使用Structured Clone傳輸數據時,有個陰影一直籠罩著我們: postMessage前要不要對數據JSON.stringify一把,聽說那樣更快?

2016年的High-performance Web Worker messages進行了測試,確實如此.但是文章的測試結果也只能停留在2016年. 2019年Surma進行新的測試:如下圖所示,橫軸上相同的數據規模,直接postMessage 的傳輸時間普遍比JSON.stringify 更少.

blank

圖片來源

2020年的當下,不需要再使用JSON.stringify .其一是Structured Clone內置的serialize/deserialize比JSON.stringify性能更高;其二是JSON.stringify只適合序列化基本數據類型,而Structured Clone還支持複製其他內置數據類型(如Map, Blob, RegExp 等, 雖然大部分應用場景只用到基本數據類型).

數據傳輸規模

我們再來聊聊Structured Clone的數據傳輸規模. Structured Clone的serialize/deserialize執行耗時主要受數據對象複雜度影響,這很好理解,因為serialize/deserialize至少要以某種方式遍歷對象.數據對象的複雜度本身難以度量, 可以用序列化後的數據規模(size)作為參考.

2015年的How fast are web workers中等性能手機上進行了測試: postMessage發送數組的通信速率為80KB/ms,相當於理想渲染週期(16ms)內發送1300KB.

2019年Surma對postMessage的數據傳輸能力進行了更深入研究,具體見Is postMessage slow .高性能機器(macbook)上的測試結果如下圖所示:

blank

圖片來源

其中:

  • 測試數據為嵌套層數1到6層(payload depth,圖中縱坐標),每層節點的子節點1到6個(payload breadth,圖中橫坐標)的對象,數據規模從10B到10MB .
  • 在macbook 上, 10MB 的數據傳遞耗時47ms, 16ms 內可以傳遞1MB 級別的數據.

低性能機器(nokia2)上的測試結果如下圖所示:

blank

圖片來源

其中:

  • 在nokia2 上傳輸10MB 的數據耗時638ms, 16ms 內可以傳遞10KB 級別的數據.
  • 高性能機器和低性能機器有超過10倍的傳輸效率差距.

不管用戶側的機器性能如何, 用戶對流暢的感受是一致的: 前端同學的老朋友16ms 和100ms. Surma 兼顧低性能機型上postMessage 容易造成主線程卡頓, 提出的數據傳輸規模建議是:

  • 如果JS 代碼裡面不包括動畫渲染(100ms), 數據傳輸規模應該保持在100KB 以下;
  • 如果JS 代碼裡麵包括動畫渲染(16ms), 數據傳輸規模應該保持在10KB 以下.

筆者認為, Surma 給出的建議偏保守, 傳輸規模可以再大一些.

總之, 數據傳輸規模並沒有最佳實踐. 而是充分理解Worker postMessage 的傳輸成本, 在實際應用中, 根據業務場景去評估和控制數據規模.

兼容性

兼容性是前端技術方案評估中需要關注的問題. 對Web Worker 更是如此, 因為Worker 的多線程能力, 要么業務場景完全用不上; 要么一用就是重度依賴的基礎能力.

blank

兼容性還不錯

從前文Worker的歷史和兼容性視圖上看, Worker的兼容性應該挺好的.

blank

如上圖所示, 主流瀏覽器在幾年前就支持Worker.

PC端:

  • IE10(2012/09)
  • Chrome4(2010/01)
  • Safari4(2009)
  • Firefox3.5(2009)

移動端:

  • iOS5(2012)
  • Android4.4(2013)

可用性評估指標

使用Worker 並不是一錘子買賣, 我們不止關注瀏覽器Worker 能力的有或沒有; 也關注Worker 能力是否完備可用. 為此筆者設計了以下幾個指標來評估Worker 可用性:

  • 是否有Worker能力:通過瀏覽器是否有window.Worker來判斷.
  • 能否實例化Worker :通過監控new Worker()是否報錯來判斷.
  • 能否跨線程通信:通過測試雙向通信來驗證,並設置超時.
  • 首次通信耗時:頁面開始加載Worker腳本到首次通訊完成的耗時.該指標與JS資源加載時長,同步邏輯執行耗時相關.

統計數據

有了可用性評估指標,就可以給出量化的兼容性統計數據.你將看到的,是開放社區上唯一一份量化數據, 2019~2020年某大型前端項目(億級MAU)的統計結果(By AlloyTeam alloy-worker ).

blank

其中:

  • 有Worker 能力的終端超過99.91%.
  • Worker 能力完全可用的終端達到99.58%.
  • 而且99.58% 到99.91% 的差距大部分由於通信超時.

小結

可見當下瀏覽器已經較好地支持Worker,只要對0.09%的不支持瀏覽器做好回退策略(如展示一個tip), Worker可以放心地應用到前端業務中.

調試工具用法

前端工程師對Worker多線程開發方式比較陌生,對開發中的Worker代碼調試也是如此.本章以Chrome和IE10為例簡單介紹調試工具用法.範例頁面為 alloyteam.github.io/all ,感興趣的同學可以打開頁面調試一把.

Chrome 調試

Chrome 已完善支持Worker 代碼調試, 開發者面板中的調試方式與主線程JS 一致.

Console 調試

Console Panel中可以查看頁面全部的JS運行環境,並通過下拉框切換調試的當前環境.如下圖所示,其中top表示主線程的JS運行環境, alloyWorker--test表示Worker線程的JS運行環境.

blank

切換到alloyWorker--test後,就可以在Worker運行環境中執行調試代碼.如下圖所示, Worker環境的全局對象為self ,類型為DedicatedWorkerGlobalScope .

blank

斷點調試

Worker斷點調試方式和主線程一致:源碼中添加debugger標識的代碼位置會作為斷點.在Sources Panel查看頁面源碼時,如下圖所示,左側面板展示Worker線程的alloy-worker.js資源;運行到Worker線程斷點時,右側的Threads提示所在的運行環境是名為alloyWorker--test的Worker線程.

blank

性能調試

使用Performance Panel 的錄製功能即可. 如下圖紅框所示, Performance 中也記錄了Worker 線程的運行情況.

blank

查看內存佔用

Worker的使用場景偏向數據和運算,開發中適時回顧Worker線程的內存佔用,避免內存洩露干擾整個Render Process.如下圖所示,在Memory Panel中alloyWorker-test線程佔用的內存為1.2M.

blank

IE10 調試

在比較極端的情況下,我們需要到IE10這種老舊的瀏覽器上定位代碼兼容性問題.好在IE10也支持Worker源碼調試.可以參考微軟官方文檔,具體步驟為:

  • F12打開調試工具,在Script Panel中,開始是看不到Worker線程源碼的,點擊Start debugging ,就能看到Worker線程的alloy-worker.js源碼.
blank
  • 在Worker 源碼上打斷點, 就能進行調試.
blank

數據流調試

跨線程通信數據流是開發和調試中比較複雜的部分. 因為頁面上可能有多個Worker 實例; Worker 實例上有不同的數據類型(payload); 而且相同類型的通信可能會多次發起.

通過onmessage回調打log調試數據流時,建議添加當前Worker實例名稱,通信類型,通信負載等訊息.以alloy-worker調試模式的log為例:

blank

如上圖所示:

  • 每行訊息包括: 線程名稱, [時間戳, 會話Id, 事務類型, 事務負載].
  • 綠色的向下箭頭(⬇)表示Worker 線程收到的訊息.
  • 粉紅的向上箭頭(⬆)表示Worker 線程發出的訊息.

社區配套工具

現代化前端開發都採用模塊化的方式組織代碼,使用Web Worker需將模塊源碼構建為單一資源( worker.js ).另一方面, Worker原生的postMessage/onmessage通信API在使用上並不順手,複雜場景下往往需要進行通信封裝和數據約定.

因此,開源社區提供了相關的配套工具,主解決2個關鍵問題:

  • Worker代碼打包.將模塊化的多個文件,打包為單一JS資源.
  • Worker通信封裝.封裝多線程通信,簡化調用;或約定通信負載的數據格式.

下面介紹社區的一些主要工具, star 數統計時間為2020.06.

worker-loader (1.1k star)

Webpack 官方的Worker loader. 負責將Worker 源碼打包為單個chunk; chunk 可以是獨立文件, 或inline 的Blob 資源.
輸出內嵌new Worker()的function,通過調用該function實例化Worker.

但worker-loader沒有提供構建後的Worker資源url,上層業務進行定制有困難.已有相關issue討論該問題; worker-loader也不對通信方式做額外處理.

worker-plugin (1.6k star)

blank

GoogleChromeLabs 提供的Webpack 構建plugin.

作為plugin,支持Worker和SharedWorker的構建.無需入侵源碼,通過解析源碼中new Workernew SharedWorker語法,自動完成JS資源的構建打包.也提供loader功能:打包資源並且返回資源url,這點比worker- loader 有優勢.

comlink (6.2k star)

blank

也來自GoogleChromeLabs 團隊, 由Surma 開發. 基於ES6 的Proxy 能力, 對postMessage 進行RPC
(Remote Procedure Call) 封裝, 將跨線程的函數調用封裝為Promise 調用.

但它不涉及Worker 資源構建打包, 需要其他配套工具. 且Proxy 在部分瀏覽器中需要polyfill, 可polyfill 程度存疑.

workerize-loader (1.7k star)

blank

目前社區比較完整,且兼容性好的方案.

類似worker-loader + comlink 的合體. 但不是基於Proxy, 而在構建時根據源碼AST 提取出調用函數名稱, 在另一線程內置同名函數; 封裝跨線程函數為RPC 調用.

與workerize-loader關聯的另一個項目是workerize (3.8k star).支持手寫文本函數,內部封裝為RPC;但手寫文本函數實用性不強.

userWorker (1.8k star)

很有趣的項目,將Worker封裝為React Hook.基本原理是:將傳入Hook的函數處理為BlobUrl去實例化Worker.因為會把函數轉為BlobUrl的字符串形式,限制了函數不能有外部依賴,函數體中也不能調用其他函數.

比較適合一次性使用的純函數, 函數複雜度受限.

其他可參考項目

現有工具缺陷

現有的社區工具解決了Worker 技術應用上的一些難點, 但目前還有些不足:

  • Web Worker 並不是100% 可用的, 社區工具並沒有給出回退方案.
  • 對大規模使用的場景, 代碼的組織架構和構建方式並沒較好的方案.
  • 部分工具在通信數據約定上缺乏強約束, 可能導致運行時意外的錯誤.
  • 支持TypeScript 源碼的較少, 編輯器中的函數提示也有障礙.

以上不足促使筆者開源了alloy-worker ,面向事務的高可用Web Worker通信框架.
更加詳細的工具討論,請查閱alloy-worker的業界方案對比.

業界實踐回顧

實踐場景

Web Worker 作為瀏覽器多線程技術, 在頁面內容不斷豐富, 功能日趨複雜的當下, 成為緩解頁面卡頓, 提升應用性能的可選方案.

2010 年

2010年,文章The Basics of Web Workers列舉的Worker可用場景如下:

blank

2010 年的應用場景主要涉及數據處理, 文本處理, 圖像/視頻處理, 網絡處理等.

當下

2018年,文章Parallel programming in JavaScript using Web Workers列舉的Worker可用場景如下:

blank

可見, 近年來Worker 的場景比2010 年更豐富, 拓展到了Canvas drawing(離屏渲染方面), Virtual DOM diffing(前端框架方面), indexedDB(本地存儲方面), Webassembly(編譯型語言方面)等.

總的來說, Worker 對頁面的計算任務/後台任務有用武之地. 接下來筆者將分享的一些具體case, 並進行簡析.

重度計算場景

石墨表格之Web Worker應用實戰

2017年的文章,非常好的實踐.在線表格排序是CPU密集型場景,複雜任務原子化和異步化後依然難以消除頁面卡頓.將排序遷移到Worker後,對2500行數據的排序操作, Scripting時間從9984ms減少到3650ms .

Making TensorflowJS work faster with WebWorkers

2020年的文章,使用生動的圖例說明TF.js在主線程運行造成的掉幀.以實時攝像頭視頻的動作檢測為例子,通過Worker實現視頻動畫不卡頓(16ms內);動作檢測耗時50ms , 但是不阻塞視頻, 也有約15FPS.

blank

騰訊文檔Excel 函數實踐

筆者撰寫文章中, 近期發布.

前端框架場景

neo -- webworkers driven UI framework

2019年開源的Worker驅動前端框架.其將前端框架的拆分為3個Worker: App Worker, Data Worker和Vdom Worker.主線程只需要維護DOM和代理DOM事件到App Worker中; Data Worker負責進行後台請求和託管數據store; Vdom Worker 將模板字符串轉換為虛擬節點, 並對每次變化生成增量去更新.

blank

worker-dom

Google AMP項目一部分.在Worker中實現DOM操作API和DOM事件監聽,並將DOM變化應用到主線程真實DOM上.官方Demo在Worker中直接引入React並實現render!

Angular

Angular8 CLI支持創建Web Worker指令,並將耗CPU計算遷移到Worker中;但是Angular本身並不能在Worker中運行.官網angular.io也用Worker來提升搜索性能.

blank

數據流場景

Off-main-thread React Redux with Performance

2019年的文章.將Reduxaction部分遷移到Worker中,開源了項目redux-in-worker .
做了Worker Redux 的benchmark: 和主線程相差不大(但是不卡了).

Off Main Thread Architecture with Vuex

2019年的文章.簡單分析UI線程過載和Worker並發能力.對Vue數據流框架Vuex進行分解,發現action可以包含異步操作,適合遷移到Worker.實現了action的封裝函數和質數生成的demo.

可視化場景

PROXX

PROXX是GoogleChromeLabs開發的在線掃雷遊戲,其Worker能力由Surma開發的Comlink提供. Surma特地開發了Worker版本和非Worker版本:在高性能機型Pixel3和MacBook上,兩者差異不大;但在低性能機型Nokia2上,非Worker版本點擊動作卡了6.6s, Worker版本點擊回調需要48ms .

圖片風格處理

2013年的文章.使用Worker將圖片處理為複古色調.在當年先進的12核機器上,使用4個Worker線程後,處理時間從150ms減低到80ms ;在當年的雙核機器上,處理時間從900ms減低到500ms.

blank

OpenCV directly in the browser (webassembly + webworker)

2020 的文章. 基於OpenCV 項目, 將項目編譯為webassembly, 並且在Worker 中動態加載opencv.js, 實現了圖片的灰度處理.

大型項目

OffscreenCanvas

Chrome69+支持,能將主線程Canvas的繪製權transfer給Worker線程的OffscreenCanvas,在Worker中繪製後渲染直接到頁面上;也支持在Worker中新建Canvas繪製圖形,通過imagebitmap transfer到主線程展示.

blank

hls.js

hls是基於JS實現的HTTP實時流媒體播放庫.其使用Worker用於流數據的解復用(demuxer) ,使用Transfer Memory來最小化傳輸的消耗.

pdf.js

判斷瀏覽器是否支持Worker能力,有Worker能力時將pdf文件解析( parsed and interpreted) 全部放在Worker線程中; Worker能力不完備則在主線程運行.

相關視頻/分享PPT

Web Workers -- I like the way you work it

2016年的分享ppt, Pokedex.org項目在Web Worker中進行Virtual DOM的更新,顯著提升快速滾動下的渲染效率.

blank

The main thread is overworked & underpaid

Chrome Dev Summit 2019,非常精彩的分享,來自google的工程師Surma.演講指出頁面主線程工作量過大,特別是發展台灣家有大量的低性能設備.運算在Worker慢一點但頁面不掉幀優於運算在主線程快一點但卡頓.

Is postMessage slow? - HTTP 203

同樣來自Surma的技術訪談.主要討論postMessage的性能問題.本文在通信速度部分大量引用Surma的研究.

Surma 在Worker 領域寫了多篇文章, 並開源了Comlink.

前端項目上Web Worker實踐

2019 年的演講, 筆者前同事, 曾在Worker 實踐上緊密合作. 演講討論Web Worker 的使用場景; Worker 的注意點和適應多線程的代碼改造; 以及實踐中遇到的問題和解決方案.

Weaving Webs of Workers

2019年的演講,來自Netflix的工程師.總結使用Web Worker遇到的4大問題,並通過引入社區多個配套工具逐一解決.

Web Workers: A graphical introduction

2018年的演講,講多線程和postMessage數據傳遞部分圖很漂亮.將Web Worker應用在他開發的Web鋼琴彈奏器.

blank

What the heack is the event Loop anyway

2014年的演講, 使用生動的圖例介紹主線程Event Loop.

實踐建議

如上文所述, 社區已有許多Worker 技術的應用實踐. 如果你的業務也有使用Worker 的需求, 以下是幾個實踐的建議.

也許你不需要Worker

使用Worker是有成本的: Worker線程會佔用系統資源;同構代碼和異步通信會增加維護成本;多線程編程會挑戰前端仔的思維.

blank

David的文章指出,迫切需要Worker的場景並不多,開發者需要考慮投入效益比.簡單來說,如果頁面的某個操作會耗時,同時不想讓用戶察覺(轉菊花),那就用Worker吧.

Worker 應該是常駐線程

雖然Worker規範提供了terminate API來結束Worker線程,但線程的頻繁新建會消耗資源.大多數場景下, Worker線程應該用作常駐的線程.開發中優先復用常駐線程.

控制Worker 線程數目

這也很好理解, Worker線程在爭取CPU計算資源時,受限於CPU的核心數,過多的線程並不能線性地提升性能,而每個Worker線程會有約1M的固有內存消耗.

理解多線程開發方式

多線程開發的思維和方式, 是個比較大的話題. 開發者需要控制線程間的通信規模, 減少線程間數據和狀態的依賴, 嘗試去了解和控制Worker 線程.

展望

本文試圖梳理2020 年當下Web Worker 技術的現狀和發展.

從現狀上看, Worker 已經普遍可用, 業界也有業務和框架上的實踐, 但在配套工具上仍有不足.

從發展趨勢上看, Worker 的多線程能力有望成為複雜前端項目的標配, 在減少UI 線程卡頓和壓榨計算機性能上有收益. 但目前國內實踐較少, 一方面是業務複雜程度未觸及;另一方面是社區缺少科普和實踐分享.

前端多線程開發正當時.筆者維護的Worker通信框架alloy-worker已經開源,大型前端項目落地的文章正在路上.雞湯和勺子都給了,加點老乾媽,真香!

References

  • alloy-worker

github.com/AlloyWorker/

  • Web Workers API

developer.mozilla.org/e

  • Remove shared workers?

whatwg/html#315

  • Using web Workers

developer.mozilla.org/e

  • Web Workers Working Draft

w3.org/TR/workers/

  • using web workers: working smarter, not harder (2009 年firefox 上的實踐)

hacks.mozilla.org/2009/

  • Is postMessage slow? (數據通信實驗設計)

dassur.ma/things/is-pos

  • 另眼看Web Worker (討論異步化編程)

ithome.com.tw/voice/132

  • The Basics of Web Workers (2010, 談到錯誤處理和安全限制)

html5rocks.com/en/tutor

  • Blink Workers (Blink 框架Worker 實現介紹)

docs.google.com/documen

  • Should you be using Web Workers (配圖非常棒)

medium.com/@david.gilbe

  • How JavaScript works

blog.sessionstack.com/h

  • Parallel programming in JavaScript using Web Workers

itnext.io/achieving-par

  • So you want to use a Web Worker

povioremote.com/blog/so

EOF

What do you think?

Written by marketer

blank

深入理解Angular 編譯器

blank

swc-node, 最快的TypeScript/JavaScript compiler