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)完成.
⇈ 圖片來源
可以直觀地聯想:並行可能會提升執行效率;運行任務拆分能減少頁面卡頓.後面應用場景章節將繼續討論.
技術規範
Web Worker屬於HTML規範,規範文檔見Web Workers Working Draft ,有興趣的同學可以讀一讀.而它並不是很新的技術,如下圖所示: 2009年就提出了草案.
⇈ 圖片來源
同年在FireFox3.5上率先實現,可以在using web workers: working smarter, not harder中看到早期的實踐. 2012年發布的IE10也實現了Web Worker,標誌著主流瀏覽器上的全面支持. IE10的Web Worker 能力測試如下圖所示:
⇈ 圖片來源
在預研Worker方案時,開發人員會有兼容性顧慮.這種顧慮的普遍存在,主要由於業界Worker技術實踐較少和社區推廣不活躍.單從發展歷史看, Worker從2012年起就廣泛可用;後面兼容性章節將繼續討論.
DedicatedWorker 和SharedWorker
Web Worker規範中包括: DedicatedWorker和SharedWorker ;規範並不包括Service Worker,本文也不會展開討論.
⇈ 圖片來源
如上圖所示, DedicatedWorker簡稱Worker,其線程只能與一個頁面渲染進程(Render Process)進行綁定和通信,不能多Tab共享. DedicatedWorker是最早實現並最廣泛支持的Web Worker能力.
而SharedWorker可以在多個瀏覽器Tab中訪問到同一個Worker實例,實現多Tab共享數據,共享webSocket連接等.看起來很美好,但safari放棄了SharedWorker支持,因為webkit引擎的技術原因.如下圖所示, 只在safari 5~6 中短暫支持過.
⇈ 圖片來源
社區也在討論是否繼續支持SharedWorker ;多Tab共享資源的需求建議在Service Worker上尋找方案.
相比之下, DedicatedWorker 有著更廣的兼容性和更多業務落地實踐, 本文後面討論中的Worker 都是特指DedicatedWorker.
主線程和多線程
用戶使用瀏覽器一般會打開多個頁面(多Tab),現代瀏覽器使用單獨的進程(Render Process)渲染每個頁面,以提升頁面性能和穩定性,並進行操作系統級別的內存隔離.
⇈ 圖片來源
主線程(Main Thread)
頁面內,內容渲染和用戶交互主要由Render Process中的主線程進行管理.主線程渲染頁面每一幀(Frame),如下圖所示,會包含5個步驟: JavaScript → Style → Layout → Paint → Composite , 如果JS 的執行修改了DOM, 可能還會暫停JS, 插入並執行Style 和Layout.
⇈ 圖片來源
而我們熟知的JS單線程和Event Loop ,是主線程的一部分. JS單線程執行避免了多線程開發中的複雜場景(如競態和死鎖).但單線程的主要困擾是:主線程同步JS執行耗時過久時(瀏覽器理想幀間隔約16ms),會阻塞用戶交互和頁面渲染.
⇈ 圖片來源
如上圖所示, 長耗時任務執行時, 頁面將無法更新, 也無法響應用戶的輸入/點擊/滾動等操作. 如果卡死太久, 瀏覽器可能會拋出卡頓的提示. 如下圖所示.
- Chrome81
- IE11
多線程
Web Worker會創建操作系統級別的線程.
The Worker interface spawns real OS-level threads . -- MDN
JS 多線程, 是有獨立於主線程的JS 運行環境. 如下圖所示: Worker 線程有獨立的內存空間, Message Queue, Event Loop, Call Stack 等, 線程間通過postMessage 通信.
多個線程可以並發運行JS.熟悉JS異步編程的同學可能會說, setTimeout
/ Promise.all
不就是並發嗎,我寫得可溜了.
JS單線程中的"並發",準確來說是Concurrent
.如下圖所示,運行時只有一個函數調用棧,通過Event Loop實現不同Task的上下文切換(Context Switch).這些Task通過BOM API調起其他線程為主線程工作,但回調函數代碼邏輯依然由JS串行運行.
Web Worker是JS多線程運行技術,準確來說是Parallel
.其與Concurrent
的區別如下圖所示: Parallel有多個函數調用棧,每個函數調用棧可以獨立運行Task,互不干擾.
應用場景
討論完主線程和多線程, 我們能更好地理解Worker 多線程的應用場景:
- 可以減少主線程卡頓.
- 可能會帶來性能提升.
減少卡頓
根據Chrome團隊提出的用戶感知性能模型RAIL ,同步JS執行時間不能過長.量化來說,播放動畫時建議小於16ms,用戶操作響應建議小於100ms,頁面打開到開始呈現內容建議小於1000ms.
邏輯異步化
減少主線程卡頓的主要方法為異步化執行,比如播放動畫時,將同步任務拆分為多個小於16ms的子任務,然後在頁面每一幀前通過requestAnimationFrame
按計劃執行一個子任務,直到全部子任務執行完畢.
⇈ 圖片來源
拆分同步邏輯的異步方案對大部分場景有效果, 但並不是一勞永逸的銀彈. 有以下幾個問題:
- 不是所有JS邏輯都可拆分.比如數組排序,樹的遞歸查找,圖像處理算法等,執行中需要維護當前狀態,且調用上非線性,無法輕易地拆分為子任務.
- 可以拆分的邏輯難以把控粒度.如下圖所示,拆分的子任務在高性能機器(iphoneX)上可以控制在16ms內,但在性能落後機器(iphone6)上就超過了deadline. 16ms的用戶感知時間,並不會因為用戶手上機器的差別而變化, Google給出的建議是再拆小到3-4ms .
⇈ 圖片來源
- 拆分的子任務並不穩定.對同步JS邏輯的拆分,需要根據業務場景尋找原子邏輯,而原子邏輯會跟隨業務變化,每次改動業務都需要去review原子邏輯.
Worker 一步到位
Worker的多線程能力,使得同步JS任務的拆分一步到位:從宏觀上將整個同步JS任務異步化.不需要再去苦苦尋找原子邏輯,邏輯異步化的設計上也更加簡單和可維護.
這給我們帶來更多的想像空間. 如下圖所示, 在瀏覽器主線程渲染週期內, 將可能阻塞頁面渲染的JS 運行任務(Jank Job)遷移到Worker 線程中, 進而減少主線程的負擔, 縮短渲染間隔, 減少頁面卡頓.
性能提升
Worker 多線程並不會直接帶來計算性能的提升, 能否提升與設備CPU 核數和線程策略有關.
多線程與CPU 核數
CPU 的單核(Single Core)和多核(Multi Core)離前端似乎有點遠了. 但在頁面上運用多線程技術時, 核數會影響線程創建策略.
進程是操作系統資源分配的基本單位,線程是操作系統調度CPU的基本單位.操作系統對線程能佔用的CPU計算資源有復雜的分配策略.如下圖所示:
- 單核多線程通過時間切片交替執行.
- 多核多線程可在不同核中真正並行.
Worker 線程策略
一台設備上相同任務在各線程中運行耗時是一樣的. 如下圖所示: 我們將主線程JS 任務交給新建的Worker 線程, 任務在Worker 線程上運行並不會比原本主線程更快, 而線程新建消耗和通信開銷使得渲染間隔可能變得更久.
⇈ 圖片來源
在單核機器上,計算資源是內捲的,新建的Worker線程並不能為頁面爭取到更多的計算資源.在多核機器上,新建的Worker線程和主線程都能做運算,頁面總計算資源增多,但對單次任務來說,在哪個線程上運行耗時是一樣的.
真正帶來性能提升的是多核多線程並發.
如多個沒有依賴關係的同步任務,在單線程上只能串行執行,在多核多線程中可以並行執行.如下圖alloy-worker的圖像處理demo所示,在iMac上運行時創建了6條Worker 線程, 圖像處理總時間比主線程串行處理快了約2000ms.
值得注意的是,目前移動設備的核心數有限.最新iPhone Max Pro上搭載的A13芯片號稱6核,也只有2個高性能核芯(2.61G),另外4個是低頻率的能效核心(0.58 G). 所以在創建多條Worker 線程時, 建議區分場景和設備.
把主線程還給UI
Worker 的應用場景, 本質上是從主線程中剝離邏輯, 讓主線程專注於UI 渲染. 這種架構設計並非Web 技術上的獨創.
Android和iOS的原生開發中, 主線程負責UI工作;前端領域熱門的小程序,實現原理上就是渲染和邏輯的完全分離.
本該如此.
Worker API
通信API
如上圖所示的Worker通信流程, Worker 通信API非常簡單.通俗中文教程可以參考Web Worker使用教程.使用細節建議看官方文檔.
雙向通信範例代碼如下圖所示,雙向通信只需7行代碼.
主要流程為:
- 主線程調用
new Worker(url)
創建Worker實例,url
為Worker JS資源url. - 主線程調用
postMessage
發送hello
,在onmesssage
中監聽Worker線程消息. - Worker線程在
onmessage
中監聽主線程消息,收到主線程的hello
;通過postMessage
回复world
. - 主線程在消息回調中收到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 .
⇈ 圖片來源
上圖展示了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 能力給業務和技術方案帶來很大的想像空間.
另一方面, 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 導致的報錯. 如下圖所示:
在開發Worker 前端項目或遷移已有業務代碼到Worker 中時, 同構代碼比例可能很高, 容易調到BOM/DOM API. 可以通過構建變量區分代碼邏輯, 或運行時動態判斷所在線程, 實現同構代碼在不同線程環境下運行.
通信速度
Worker多線程雖然實現了JS任務的並行運行,也帶來額外的通信開銷.如下圖所示,從線程A調用postMessage發送數據到線程B onmessage接收到數據有時間差,這段時間差稱為通信消耗.
⇈ 圖片來源
提升的性能= 并行提升的性能– 通信消耗的性能
.在線程計算能力固定的情況下,要通過多線程提升更多性能,需要盡量減少通信消耗.
而且主線程postMessage會佔用主線程同步執行,佔用時間與數據傳輸方式和數據規模相關.要避免多線程通信導致的主線程卡頓,需選擇合適的傳輸方式,並控制每個渲染週期內的數據傳輸規模.
數據傳輸方式
我們先來聊聊主線程和Worker 線程的數據傳輸方式. 根據計算機進程模型, 主線程和Worker 線程屬於同一進程, 可以訪問和操作進程的內存空間. 但為了降低多線程並發的邏輯複雜度, 部分傳輸方式直接隔離了線程間的內存, 相當於默認加了鎖.
通信方式有3 種: Structured Clone, Transfer Memory 和Shared Array Buffer.
Structured Clone
Structured Clone是postMessage默認的通信方式.如下圖所示,複製一份線程A的JS Object內存給到線程B,線程B能獲取和操作新復制的內存.
Structured Clone通過複製內存的方式簡單有效地隔離不同線程內存,避免衝突;且傳輸的Object數據結構很靈活.但複製過程中,線程A要同步執行Object Serialization,線程B要同步執行Object Deserialization;如果Object規模過大, 會佔用大量的線程時間.
Transfer Memory
Transfer Memory意為轉移內存,它不需要Serialization/Deserialization,能大大減少傳輸過程佔用的線程時間.如下圖所示,線程A將指定內存的所有權和操作權轉給線程B,但轉讓後線程A無法再訪問這塊內存.
Transfer Memory以失去控制權來換取高效傳輸,通過內存獨占給多線程並發加鎖.但只能轉讓ArrayBuffer等大小規整的二進制(Raw Binary)數據;對矩陣數據(如RGB圖片)比較適用.實踐上也要考慮從JS Object 生成二進制數據的運算成本.
Shared Array Buffers
Shared Array Buffer是共享內存,線程A和線程B可以同時訪問和操作同一塊內存空間.數據都共享了,也就沒有傳輸什麼事了.
但多個並行的線程共享內存,會產生競爭問題(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 更少.
⇈ 圖片來源
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)上的測試結果如下圖所示:
⇈ 圖片來源
其中:
- 測試數據為嵌套層數1到6層(payload depth,圖中縱坐標),每層節點的子節點1到6個(payload breadth,圖中橫坐標)的對象,數據規模從10B到10MB .
- 在macbook 上, 10MB 的數據傳遞耗時47ms, 16ms 內可以傳遞1MB 級別的數據.
低性能機器(nokia2)上的測試結果如下圖所示:
⇈ 圖片來源
其中:
- 在nokia2 上傳輸10MB 的數據耗時638ms, 16ms 內可以傳遞10KB 級別的數據.
- 高性能機器和低性能機器有超過10倍的傳輸效率差距.
不管用戶側的機器性能如何, 用戶對流暢的感受是一致的: 前端同學的老朋友16ms 和100ms. Surma 兼顧低性能機型上postMessage 容易造成主線程卡頓, 提出的數據傳輸規模建議是:
- 如果JS 代碼裡面不包括動畫渲染(100ms), 數據傳輸規模應該保持在100KB 以下;
- 如果JS 代碼裡麵包括動畫渲染(16ms), 數據傳輸規模應該保持在10KB 以下.
筆者認為, Surma 給出的建議偏保守, 傳輸規模可以再大一些.
總之, 數據傳輸規模並沒有最佳實踐. 而是充分理解Worker postMessage 的傳輸成本, 在實際應用中, 根據業務場景去評估和控制數據規模.
兼容性
兼容性是前端技術方案評估中需要關注的問題. 對Web Worker 更是如此, 因為Worker 的多線程能力, 要么業務場景完全用不上; 要么一用就是重度依賴的基礎能力.
兼容性還不錯
從前文Worker的歷史和兼容性視圖上看, Worker的兼容性應該挺好的.
如上圖所示, 主流瀏覽器在幾年前就支持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 ).
其中:
- 有Worker 能力的終端超過99.91%.
- Worker 能力完全可用的終端達到99.58%.
- 而且99.58% 到99.91% 的差距大部分由於通信超時.
小結
可見當下瀏覽器已經較好地支持Worker,只要對0.09%的不支持瀏覽器做好回退策略(如展示一個tip), Worker可以放心地應用到前端業務中.
調試工具用法
前端工程師對Worker多線程開發方式比較陌生,對開發中的Worker代碼調試也是如此.本章以Chrome和IE10為例簡單介紹調試工具用法.範例頁面為https:// alloyteam.github.io/all oy- worker ,感興趣的同學可以打開頁面調試一把.
Chrome 調試
Chrome 已完善支持Worker 代碼調試, 開發者面板中的調試方式與主線程JS 一致.
Console 調試
Console Panel中可以查看頁面全部的JS運行環境,並通過下拉框切換調試的當前環境.如下圖所示,其中top
表示主線程的JS運行環境, alloyWorker--test
表示Worker線程的JS運行環境.
切換到alloyWorker--test
後,就可以在Worker運行環境中執行調試代碼.如下圖所示, Worker環境的全局對象為self
,類型為DedicatedWorkerGlobalScope
.
斷點調試
Worker斷點調試方式和主線程一致:源碼中添加debugger
標識的代碼位置會作為斷點.在Sources Panel查看頁面源碼時,如下圖所示,左側面板展示Worker線程的alloy-worker.js
資源;運行到Worker線程斷點時,右側的Threads
提示所在的運行環境是名為alloyWorker--test
的Worker線程.
性能調試
使用Performance Panel 的錄製功能即可. 如下圖紅框所示, Performance 中也記錄了Worker 線程的運行情況.
查看內存佔用
Worker的使用場景偏向數據和運算,開發中適時回顧Worker線程的內存佔用,避免內存洩露干擾整個Render Process.如下圖所示,在Memory Panel中alloyWorker-test
線程佔用的內存為1.2M.
IE10 調試
在比較極端的情況下,我們需要到IE10這種老舊的瀏覽器上定位代碼兼容性問題.好在IE10也支持Worker源碼調試.可以參考微軟官方文檔,具體步驟為:
- 按
F12
打開調試工具,在Script Panel中,開始是看不到Worker線程源碼的,點擊Start debugging
,就能看到Worker線程的alloy-worker.js
源碼.
- 在Worker 源碼上打斷點, 就能進行調試.
數據流調試
跨線程通信數據流是開發和調試中比較複雜的部分. 因為頁面上可能有多個Worker 實例; Worker 實例上有不同的數據類型(payload); 而且相同類型的通信可能會多次發起.
通過onmessage回調打log調試數據流時,建議添加當前Worker實例名稱,通信類型,通信負載等訊息.以alloy-worker調試模式的log為例:
如上圖所示:
- 每行訊息包括: 線程名稱, [時間戳, 會話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)
GoogleChromeLabs 提供的Webpack 構建plugin.
作為plugin,支持Worker和SharedWorker的構建.無需入侵源碼,通過解析源碼中new Worker
和new SharedWorker
語法,自動完成JS資源的構建打包.也提供loader功能:打包資源並且返回資源url,這點比worker- loader 有優勢.
comlink (6.2k star)
也來自GoogleChromeLabs 團隊, 由Surma 開發. 基於ES6 的Proxy 能力, 對postMessage 進行RPC
(Remote Procedure Call) 封裝, 將跨線程的函數調用封裝為Promise 調用.
但它不涉及Worker 資源構建打包, 需要其他配套工具. 且Proxy 在部分瀏覽器中需要polyfill, 可polyfill 程度存疑.
workerize-loader (1.7k star)
目前社區比較完整,且兼容性好的方案.
類似worker-loader + comlink 的合體. 但不是基於Proxy, 而在構建時根據源碼AST 提取出調用函數名稱, 在另一線程內置同名函數; 封裝跨線程函數為RPC 調用.
與workerize-loader關聯的另一個項目是workerize (3.8k star).支持手寫文本函數,內部封裝為RPC;但手寫文本函數實用性不強.
userWorker (1.8k star)
很有趣的項目,將Worker封裝為React Hook.基本原理是:將傳入Hook的函數處理為BlobUrl
去實例化Worker.因為會把函數轉為BlobUrl
的字符串形式,限制了函數不能有外部依賴,函數體中也不能調用其他函數.
比較適合一次性使用的純函數, 函數複雜度受限.
其他可參考項目
- promise-worker 0.4k star.
- greenlet 4.3k star.
- workly 1.7k star.
- threads.js 1.1k star,支持nodejs.
現有工具缺陷
現有的社區工具解決了Worker 技術應用上的一些難點, 但目前還有些不足:
- Web Worker 並不是100% 可用的, 社區工具並沒有給出回退方案.
- 對大規模使用的場景, 代碼的組織架構和構建方式並沒較好的方案.
- 部分工具在通信數據約定上缺乏強約束, 可能導致運行時意外的錯誤.
- 支持TypeScript 源碼的較少, 編輯器中的函數提示也有障礙.
以上不足促使筆者開源了alloy-worker ,面向事務的高可用Web Worker通信框架.
更加詳細的工具討論,請查閱alloy-worker的業界方案對比.
業界實踐回顧
實踐場景
Web Worker 作為瀏覽器多線程技術, 在頁面內容不斷豐富, 功能日趨複雜的當下, 成為緩解頁面卡頓, 提升應用性能的可選方案.
2010 年
2010年,文章The Basics of Web Workers列舉的Worker可用場景如下:
2010 年的應用場景主要涉及數據處理, 文本處理, 圖像/視頻處理, 網絡處理等.
當下
2018年,文章Parallel programming in JavaScript using Web Workers列舉的Worker可用場景如下:
可見, 近年來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.
騰訊文檔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 將模板字符串轉換為虛擬節點, 並對每次變化生成增量去更新.
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來提升搜索性能.
數據流場景
Off-main-thread React Redux with Performance
2019年的文章.將Redux的action
部分遷移到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.
OpenCV directly in the browser (webassembly + webworker)
2020 的文章. 基於OpenCV 項目, 將項目編譯為webassembly, 並且在Worker 中動態加載opencv.js, 實現了圖片的灰度處理.
大型項目
OffscreenCanvas
Chrome69+支持,能將主線程Canvas的繪製權transfer給Worker線程的OffscreenCanvas,在Worker中繪製後渲染直接到頁面上;也支持在Worker中新建Canvas繪製圖形,通過imagebitmap transfer到主線程展示.
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, http:// Pokedex.org項目在Web Worker中進行Virtual DOM的更新,顯著提升快速滾動下的渲染效率.
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鋼琴彈奏器.
What the heack is the event Loop anyway
2014年的演講, 使用生動的圖例介紹主線程Event Loop.
實踐建議
如上文所述, 社區已有許多Worker 技術的應用實踐. 如果你的業務也有使用Worker 的需求, 以下是幾個實踐的建議.
也許你不需要Worker
使用Worker是有成本的: Worker線程會佔用系統資源;同構代碼和異步通信會增加維護成本;多線程編程會挑戰前端仔的思維.
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
- Web Workers API
https:// developer.mozilla.org/e n-US/docs/Web/API/Web_Workers_API
- Remove shared workers?
- Using web Workers
https:// developer.mozilla.org/e n-US/docs/Web/API/Web_Workers_API/Using_web_workers
- Web Workers Working Draft
- using web workers: working smarter, not harder (2009 年firefox 上的實踐)
https:// hacks.mozilla.org/2009/ 07/working-smarter-not-harder/
- Is postMessage slow? (數據通信實驗設計)
- 另眼看Web Worker (討論異步化編程)
- The Basics of Web Workers (2010, 談到錯誤處理和安全限制)
- Blink Workers (Blink 框架Worker 實現介紹)
- Should you be using Web Workers (配圖非常棒)
- How JavaScript works
- Parallel programming in JavaScript using Web Workers
https:// itnext.io/achieving-par allelism-in-javascript-using-web-workers-8f921f2d26db
- So you want to use a Web Worker
https:// povioremote.com/blog/so -you-want-to-use-a-web-worker/