做者:TAT.cnt
Web Worker 做爲瀏覽器多線程技術, 在頁面內容不斷豐富, 功能日趨複雜的當下, 成爲緩解頁面卡頓, 提高應用性能的可選方案. 但她的容顏, 隱藏在邊緣試探的科普文章和不知深淺的兼容性背後; 對 JS 單線程面試題滾瓜爛熟的前端工程師, 對多線程開發有着自然的陌生感.
Web Worker 做爲瀏覽器多線程技術, 在頁面內容不斷豐富, 功能日趨複雜的當下, 成爲緩解頁面卡頓, 提高應用性能的可選方案.但她的容顏, 隱藏在邊緣試探的科普文章和不知深淺的兼容性背後; 對 JS 單線程面試題滾瓜爛熟的前端工程師, 對多線程開發有着自然的陌生感.javascript
⇈圖片來源html
文獻綜述
(Literature Review)是學術研究領域一個常見概念, 寫過畢業論文的同窗應該還有印象. 它向讀者介紹與主題有關的詳細資料、動態、進展、展望以及對以上方面的評述.前端
近期筆者關注 Web Worker, 並落地到了大型複雜前端項目. 開源了 Worker 通訊框架 alloy-worker, 正在寫實踐總結文章. 其間查閱了相關資料(50+文章, 10+技術演講), 獨立寫成這篇綜述性文章.vue
前端同窗對 Web Worker 應該不陌生, 即便沒有動手實踐過, 應該也在社區上看過相關文章. 在介紹和使用上, 官方文檔是 MDN 的 Web Workers API. 其對 Web Worker 的表述是:html5
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) 完成.java
⇈圖片來源node
能夠直觀地聯想: 並行可能會提高執行效率; 運行任務拆分能減小頁面卡頓. 後面應用場景章節將繼續討論.react
Web Worker 屬於 HTML 規範, 規範文檔見 Web Workers Working Draft, 有興趣的同窗能夠讀一讀. 而它並非很新的技術, 以下圖所示: 2009 年就提出了草案.android
⇈圖片來源webpack
同年在 FireFox3.5 上率先實現, 能夠在 using web workers: working smarter, not harder 中看到早期的實踐. 2012年發佈的 IE10 也實現了 Web Worker, 標誌着主流瀏覽器上的全面支持. IE10 的 Web Worker 能力測試以下圖所示:
⇈圖片來源
在預研 Worker 方案時, 開發人員會有兼容性顧慮. 這種顧慮的廣泛存在, 主要因爲業界 Worker 技術實踐較少和社區推廣不活躍. 單從發展歷史看, Worker 從 2012 年起就普遍可用; 後面兼容性章節將繼續討論.
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)渲染每一個頁面, 以提高頁面性能和穩定性, 並進行操做系統級別的內存隔離.
⇈圖片來源
頁面內, 內容渲染和用戶交互主要由 Render Process 中的主線程進行管理. 主線程渲染頁面每一幀(Frame), 以下圖所示, 會包含 5 個步驟: JavaScript → Style → Layout → Paint → Composite, 若是 JS 的執行修改了 DOM, 可能還會暫停 JS, 插入並執行 Style 和 Layout.
⇈圖片來源
而咱們熟知的 JS 單線程和 Event Loop, 是主線程的一部分. JS 單線程執行避免了多線程開發中的複雜場景(如競態和死鎖). 但單線程的主要困擾是: 主線程同步 JS 執行耗時太久時(瀏覽器理想幀間隔約 16ms), 會阻塞用戶交互和頁面渲染.
⇈圖片來源
如上圖所示, 長耗時任務執行時, 頁面將沒法更新, 也沒法響應用戶的輸入/點擊/滾動等操做. 若是卡死過久, 瀏覽器可能會拋出卡頓的提示. 以下圖所示.
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
按計劃執行一個子任務, 直到所有子任務執行完畢.
⇈圖片來源
拆分同步邏輯的異步方案對大部分場景有效果, 但並非一勞永逸的銀彈. 有如下幾個問題:
⇈圖片來源
Worker 的多線程能力, 使得同步 JS 任務的拆分一步到位: 從宏觀上將整個同步 JS 任務異步化. 不須要再去苦苦尋找原子邏輯, 邏輯異步化的設計上也更加簡單和可維護.
這給咱們帶來更多的想象空間. 以下圖所示, 在瀏覽器主線程渲染週期內, 將可能阻塞頁面渲染的 JS 運行任務(Jank Job)遷移到 Worker 線程中, 進而減小主線程的負擔, 縮短渲染間隔, 減小頁面卡頓.
Worker 多線程並不會直接帶來計算性能的提高, 可否提高與設備 CPU 核數和線程策略有關.
CPU 的單核(Single Core)和多核(Multi Core)離前端彷佛有點遠了. 但在頁面上運用多線程技術時, 核數會影響線程建立策略.
進程是操做系統資源分配的基本單位,線程是操做系統調度 CPU 的基本單位. 操做系統對線程能佔用的 CPU 計算資源有複雜的分配策略. 以下圖所示:
一臺設備上相同任務在各線程中運行耗時是同樣的. 以下圖所示: 咱們將主線程 JS 任務交給新建的 Worker 線程, 任務在 Worker 線程上運行並不會比本來主線程更快, 而線程新建消耗和通訊開銷使得渲染間隔可能變得更久.
⇈圖片來源
在單核機器上, 計算資源是內卷的, 新建的 Worker 線程並不能爲頁面爭取到更多的計算資源. 在多核機器上, 新建的 Worker 線程和主線程都能作運算, 頁面總計算資源增多, 但對單次任務來講, 在哪一個線程上運行耗時是同樣的.
真正帶來性能提高的是多核多線程併發.
如多個沒有依賴關係的同步任務, 在單線程上只能串行執行, 在多核多線程中能夠並行執行. 以下圖 alloy-worker 的圖像處理 demo 所示, 在 iMac 上運行時建立了 6 條 Worker 線程, 圖像處理總時間比主線程串行處理快了約 2000ms.
值得注意的是, 目前移動設備的核心數有限. 最新 iPhone Max Pro 上搭載的 A13 芯片 號稱 6 核, 也只有 2 個高性能核芯(2.61G), 另外 4 個是低頻率的能效核心(0.58G). 因此在建立多條 Worker 線程時, 建議區分場景和設備.
Worker 的應用場景, 本質上是從主線程中剝離邏輯, 讓主線程專一於 UI 渲染. 這種架構設計並不是 Web 技術上的首創.
Android 和 iOS 的原生開發中, 主線程負責 UI 工做; 前端領域熱門的小程序, 實現原理上就是渲染和邏輯的徹底分離.
本該如此.
如上圖所示的 Worker 通訊流程, Worker 通訊 API 很是簡單. 通俗中文教程能夠參考 Web Worker 使用教程. 使用細節建議看官方文檔.
雙向通訊示例代碼以下圖所示, 雙向通訊只需 7 行代碼.
主要流程爲:
new Worker(url)
建立 Worker 實例, url
爲 Worker JS 資源 url.postMessage
發送 hello
, 在 onmesssage
中監聽 Worker 線程消息.onmessage
中監聽主線程消息, 收到主線程的 hello
; 經過 postMessage
回覆 world
.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 是無 UI 的線程, 沒法調用 UI 相關的 DOM/BOM API. Worker 具體支持的 API 可參考 MDN 的 functions and classes available to workers.
⇈圖片來源
上圖展現了 Worker 線程與主線程的異同點. Worker 運行環境與主線程的共同點主要包括:
Navigator.userAgent
識別瀏覽器.從共同點上看, Worker 線程其實很強大, 除了利用獨立線程執行重度邏輯外, 其網絡 I/O 和文件 I/O 能力給業務和技術方案帶來很大的想象空間.
另外一方面, Worker 線程運行環境和主線程的差別點有:
self.close
自行銷燬.從差別點上看, Worker 線程沒法染指 UI, 並受主線程控制, 適合默默幹活.
各家瀏覽器實現 Worker 規範有差別, 對比主線程, 部分 API 功能不完備, 如:
好在這種場景並很少. 而且能夠在運行時經過錯誤監控發現問題, 並定位和修復(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 是 postMessage 默認的通訊方式. 以下圖所示, 複製一份線程A 的 JS Object 內存給到線程B, 線程B 能獲取和操做新複製的內存.
Structured Clone 經過複製內存的方式簡單有效地隔離不一樣線程內存, 避免衝突; 且傳輸的 Object 數據結構很靈活. 但複製過程當中, 線程A 要同步執行 Object Serialization, 線程B 要同步執行 Object Deserialization; 若是 Object 規模過大, 會佔用大量的線程時間.
Transfer Memory 意爲轉移內存, 它不須要 Serialization/Deserialization, 能大大減小傳輸過程佔用的線程時間. 以下圖所示 , 線程A 將指定內存的全部權和操做權轉給線程B, 但轉讓後線程A 沒法再訪問這塊內存.
Transfer Memory 以失去控制權來換取高效傳輸, 經過內存獨佔給多線程併發加鎖. 但只能轉讓 ArrayBuffer 等大小規整的二進制(Raw Binary)數據; 對矩陣數據(如 RGB 圖片)比較適用. 實踐上也要考慮從 JS Object 生成二進制數據的運算成本.
Shared Array Buffer 是共享內存, 線程A 和線程B 能夠同時訪問和操做同一塊內存空間. 數據都共享了, 也就沒有傳輸什麼事了.
但多個並行的線程共享內存, 會產生競爭問題(Race Conditions). 不像前 2 種傳輸方式默認加鎖, Shared Array Buffers 把難題拋給開發者, 開發者能夠用 Atomics 來維護這塊共享的內存. 做爲較新的傳輸方式, 瀏覽器兼容性可想而知, 目前只有 Chrome 68+ 支持.
使用 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) 上的測試結果以下圖所示:
⇈圖片來源
其中:
低性能機器(nokia2) 上的測試結果以下圖所示:
⇈圖片來源
其中:
無論用戶側的機器性能如何, 用戶對流暢的感覺是一致的: 前端同窗的老朋友 16ms 和 100ms. Surma 兼顧低性能機型上 postMessage 容易形成主線程卡頓, 提出的數據傳輸規模建議是:
筆者認爲, Surma 給出的建議偏保守, 傳輸規模能夠再大一些.
總之, 數據傳輸規模並無最佳實踐. 而是充分理解 Worker postMessage 的傳輸成本, 在實際應用中, 根據業務場景去評估和控制數據規模.
兼容性是前端技術方案評估中須要關注的問題. 對 Web Worker 更是如此, 由於 Worker 的多線程能力, 要麼業務場景徹底用不上; 要麼一用就是重度依賴的基礎能力.
從前文 Worker 的歷史和 兼容性視圖 上看, Worker 的兼容性應該挺好的.
如上圖所示, 主流瀏覽器在幾年前就支持 Worker.
PC端:
移動端:
使用 Worker 並非一錘子買賣, 咱們不止關注瀏覽器 Worker 能力的有或沒有; 也關注 Worker 能力是否完備可用. 爲此筆者設計瞭如下幾個指標來評估 Worker 可用性:
window.Worker
來判斷.new Worker()
是否報錯來判斷.有了可用性評估指標, 就能夠給出量化的兼容性統計數據. 你將看到的, 是開放社區上惟一一分量化數據, 2019~2020 年某大型前端項目(億級 MAU)的統計結果(By AlloyTeam alloy-worker).
其中:
可見當下瀏覽器已經較好地支持 Worker, 只要對 0.09% 的不支持瀏覽器作好回退策略(如展現一個 tip), Worker 能夠放心地應用到前端業務中.
前端工程師對 Worker 多線程開發方式比較陌生, 對開發中的 Worker 代碼調試也是如此. 本章以 Chrome 和 IE10 爲例簡單介紹調試工具用法. 示例頁面爲 https://alloyteam.github.io/alloy-worker, 感興趣的同窗能夠打開頁面調試一把.
Chrome 已完善支持 Worker 代碼調試, 開發者面板中的調試方式與主線程 JS 一致.
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 也支持 Worker 源碼調試. 能夠參考微軟官方文檔, 具體步驟爲:
F12
打開調試工具, 在 Script Panel 中, 開始是看不到 Worker 線程源碼的, 點擊 Start debugging
, 就能看到 Worker 線程的 alloy-worker.js
源碼.跨線程通訊數據流是開發和調試中比較複雜的部分. 由於頁面上可能有多個 Worker 實例; Worker 實例上有不一樣的數據類型(payload); 並且相同類型的通訊可能會屢次發起.
經過 onmessage 回調打 log 調試數據流時, 建議添加當前 Worker 實例名稱, 通訊類型, 通訊負載等信息. 以 alloy-worker 調試模式的 log 爲例:
如上圖所示:
現代化前端開發都採用模塊化的方式組織代碼, 使用 Web Worker 需將模塊源碼構建爲單一資源(worker.js
). 另外一方面, Worker 原生的 postMessage/onmessage
通訊 API 在使用上並不順手, 複雜場景下每每須要進行通訊封裝和數據約定.
所以, 開源社區提供了相關的配套工具, 主解決 2 個關鍵問題:
下面介紹社區的一些主要工具, star 數統計時間爲 2020.06.
Webpack 官方的 Worker loader. 負責將 Worker 源碼打包爲單個 chunk; chunk 能夠是獨立文件, 或 inline 的 Blob 資源.
輸出內嵌 new Worker()
的 function, 經過調用該 function 實例化 Worker.
但 worker-loader 沒有提供構建後的 Worker 資源 url, 上層業務進行定製有困難. 已有相關 issue 討論該問題; worker-loader 也不對通訊方式作額外處理.
GoogleChromeLabs 提供的 Webpack 構建 plugin.
做爲 plugin, 支持 Worker 和 SharedWorker 的構建. 無需入侵源碼, 經過解析源碼中 new Worker
和 new SharedWorker
語法, 自動完成 JS 資源的構建打包. 也提供 loader 功能: 打包資源而且返回資源 url, 這點比 worker-loader 有優點.
也來自 GoogleChromeLabs 團隊, 由 Surma 開發. 基於 ES6 的 Proxy 能力, 對 postMessage 進行 RPC
(Remote Procedure Call) 封裝, 將跨線程的函數調用封裝爲 Promise 調用.
但它不涉及 Worker 資源構建打包, 須要其餘配套工具. 且 Proxy 在部分瀏覽器中須要 polyfill, 可 polyfill 程度存疑.
目前社區比較完整, 且兼容性好的方案.
相似 worker-loader + comlink 的合體. 但不是基於 Proxy, 而在構建時根據源碼 AST 提取出調用函數名稱, 在另外一線程內置同名函數; 封裝跨線程函數爲 RPC 調用.
與 workerize-loader 關聯的另外一個項目是 workerize (3.8k star). 支持手寫文本函數, 內部封裝爲 RPC; 但手寫文本函數實用性不強.
頗有趣的項目, 將 Worker 封裝爲 React Hook. 基本原理是: 將傳入 Hook 的函數處理爲 BlobUrl
去實例化 Worker. 由於會把函數轉爲 BlobUrl
的字符串形式, 限制了函數不能有外部依賴, 函數體中也不能調用其餘函數.
比較適合一次性使用的純函數, 函數複雜度受限.
現有的社區工具解決了 Worker 技術應用上的一些難點, 但目前還有些不足:
以上不足促使筆者開源了 alloy-worker, 面向事務的高可用 Web Worker 通訊框架.
更加詳細的工具討論, 請查閱 alloy-worker 的業界方案對比.
Web Worker 做爲瀏覽器多線程技術, 在頁面內容不斷豐富, 功能日趨複雜的當下, 成爲緩解頁面卡頓, 提高應用性能的可選方案.
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, 並進行簡析.
2017 年的文章, 很是好的實踐. 在線表格排序是 CPU 密集型場景, 複雜任務原子化和異步化後依然難以消除頁面卡頓. 將排序遷移到 Worker 後, 對 2500 行數據的排序操做, Scripting 時間從 9984ms 減小到 3650ms .
2020 年的文章, 使用生動的圖例說明 TF.js 在主線程運行形成的掉幀. 以實時攝像頭視頻的動做檢測爲例子, 經過 Worker 實現視頻動畫不卡頓(16ms內); 動做檢測耗時 50ms, 可是不阻塞視頻, 也有約 15FPS.
筆者撰寫文章中, 近期發佈.
2019 年開源的 Worker 驅動前端框架. 其將前端框架的拆分爲 3 個 Worker: App Worker, Data Worker 和 Vdom Worker. 主線程只須要維護 DOM 和代理 DOM 事件到 App Worker 中; Data Worker 負責進行後臺請求和託管數據 store; Vdom Worker 將模板字符串轉換爲虛擬節點, 並對每次變化生成增量去更新.
Google AMP 項目一部分. 在 Worker 中實現 DOM 操做 API 和 DOM 事件監聽, 並將 DOM 變化應用到主線程真實 DOM 上. 官方 Demo 在 Worker 中直接引入 React 並實現 render!
Angular8 CLI 支持建立 Web Worker 指令, 並將耗 CPU 計算遷移到 Worker 中; 可是 Angular 自己並不能在 Worker 中運行. 官網 angular.io 也用 Worker 來提高搜索性能.
2019 年的文章. 將 Redux 的 action
部分遷移到 Worker 中, 開源了項目 redux-in-worker.
作了 Worker Redux 的 benchmark: 和主線程相差不大(可是不卡了).
2019 年的文章. 簡單分析 UI 線程過載和 Worker 併發能力. 對 Vue 數據流框架 Vuex 進行分解, 發現 action
能夠包含異步操做, 適合遷移到 Worker. 實現了 action 的封裝函數和質數生成的 demo.
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.
2020 的文章. 基於 OpenCV 項目, 將項目編譯爲 webassembly, 而且在 Worker 中動態加載 opencv.js, 實現了圖片的灰度處理.
Chrome69+ 支持, 能將主線程 Canvas 的繪製權 transfer 給 Worker 線程的 OffscreenCanvas, 在 Worker 中繪製後渲染直接到頁面上; 也支持在 Worker 中新建 Canvas 繪製圖形, 經過 imagebitmap transfer 到主線程展現.
hls 是基於 JS 實現的 HTTP 實時流媒體播放庫. 其使用 Worker 用於流數據的解複用(demuxer), 使用 Transfer Memory 來最小化傳輸的消耗.
判斷瀏覽器是否支持 Worker 能力, 有 Worker 能力時將 pdf 文件解析放在 Worker 線程中.
2016年的分享 ppt, Pokedex.org 項目在 Web Worker 中進行 Virtual DOM 的更新, 顯著提高快速滾動下的渲染效率.
Chrome Dev Summit 2019, 很是精彩的分享, 來自 google 的工程師 Surma. 演講指出頁面主線程工做量過大, 特別是發展中國家有大量的低性能設備. 運算在 Worker 慢一點但頁面不掉幀優於運算在主線程快一點但卡頓.
一樣來自 Surma 的技術訪談. 主要討論 postMessage 的性能問題. 本文在通訊速度部分大量引用 Surma 的研究.
Surma 在 Worker 領域寫了多篇文章, 並開源了 Comlink.
2019 年的演講, 筆者前同事, 曾在 Worker 實踐上緊密合做. 演講討論 Web Worker 的使用場景; Worker 的注意點和適應多線程的代碼改造; 以及實踐中遇到的問題和解決方案.
2019 年的演講, 來自 Netflix 的工程師. 總結使用 Web Worker 遇到的 4 大問題, 並經過引入社區多個配套工具逐一解決.
2018年的演講, 講多線程和 postMessage 數據傳遞部分圖很漂亮. 將 Web Worker 應用在他開發的 Web 鋼琴彈奏器.
2014年的演講, 使用生動的圖例介紹主線程 Event Loop.
如上文所述, 社區已有許多 Worker 技術的應用實踐. 若是你的業務也有使用 Worker 的需求, 如下是幾個實踐的建議.
使用 Worker 是有成本的: Worker 線程會佔用系統資源; 同構代碼和異步通訊會增長維護成本; 多線程編程會挑戰前端仔的思惟.
David 的文章指出, 迫切須要 Worker 的場景並很少, 開發者須要考慮投入效益比. 簡單來講, 若是頁面的某個操做會耗時, 同時不想讓用戶察覺(轉菊花), 那就用 Worker 吧.
雖然 Worker 規範提供了 terminate
API 來結束 Worker 線程, 但線程的頻繁新建會消耗資源. 大多數場景下, Worker 線程應該用做常駐的線程. 開發中優先複用常駐線程.
這也很好理解, Worker 線程在爭取 CPU 計算資源時, 受限於 CPU 的核心數, 過多的線程並不能線性地提高性能, 而每一個 Worker 線程會有約 1M 的固有內存消耗.
多線程開發的思惟和方式, 是個比較大的話題. 開發者須要控制線程間的通訊規模, 減小線程間數據和狀態的依賴, 嘗試去了解和控制 Worker 線程.
本文試圖梳理 2020 年當下 Web Worker 技術的現狀和發展.
從現狀上看, Worker 已經廣泛可用, 業界也有業務和框架上的實踐, 但在配套工具上仍有不足.
從發展趨勢上看, Worker 的多線程能力有望成爲複雜前端項目的標配, 在減小 UI 線程卡頓和壓榨計算機性能上有收益. 但目前國內實踐較少, 一方面是業務複雜程度未觸及; 另外一方面是社區缺乏科普和實踐分享.
前端多線程開發正當時. 筆者維護的 Worker 通訊框架 alloy-worker 已經開源, 大型前端項目落地的文章正在路上. 雞湯和勺子都給了, 加點老乾媽, 真香!
https://github.com/AlloyWorker/alloy-worker
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
whatwg/html#315
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
https://www.w3.org/TR/workers/
https://hacks.mozilla.org/2009/07/working-smarter-not-harder/
https://dassur.ma/things/is-postmessage-slow/
https://www.ithome.com.tw/voice/132997
https://www.html5rocks.com/en/tutorials/workers/basics/
https://docs.google.com/document/d/1i3IA3TG00rpQ7MKlpNFYUF6EfLcV01_Cv3IYG_DjF7M/edit#heading=h.7smox3ra3f6n
https://medium.com/@david.gilbertson/should-you-should-be-using-web-workers-hint-probably-not-9b6d26dc8c6a
https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a
https://itnext.io/achieving-parallelism-in-javascript-using-web-workers-8f921f2d26db
https://povioremote.com/blog/so-you-want-to-use-a-web-worker/
AlloyTeam 歡迎優秀的小夥伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)