Web Worker 文獻綜述

做者:TAT.cnt

導語

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

Web Worker 文獻綜述

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 年起就普遍可用; 後面兼容性章節將繼續討論.

DedicatedWorker 和 SharedWorker

Web Worker 規範中包括: DedicatedWorkerSharedWorker; 規範並不包括 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.58G). 因此在建立多條 Worker 線程時, 建議區分場景和設備.

把主線程還給 UI

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

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

本該如此.

Worker API

通訊 API

如上圖所示的 Worker 通訊流程, Worker 通訊 API 很是簡單. 通俗中文教程能夠參考 Web Worker 使用教程. 使用細節建議看官方文檔.

雙向通訊示例代碼以下圖所示, 雙向通訊只需 7 行代碼.

主要流程爲:

  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.

圖片來源

上圖展現了 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/alloy-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 Workernew 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 的字符串形式, 限制了函數不能有外部依賴, 函數體中也不能調用其餘函數.

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

其餘可參考項目

現有工具缺陷

現有的社區工具解決了 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 年的文章. 將 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.

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 文件解析放在 Worker 線程中.

相關視頻/分享 PPT

Web Workers -- I like the way you work it

2016年的分享 ppt, 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
https://github.com/AlloyWorker/alloy-worker
  • Web Workers API
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
  • Remove shared workers?
whatwg/html#315
  • Using web Workers
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
  • Web Workers Working Draft
https://www.w3.org/TR/workers/
  • using web workers: working smarter, not harder (2009 年 firefox 上的實踐)
https://hacks.mozilla.org/2009/07/working-smarter-not-harder/
  • Is postMessage slow? (數據通訊實驗設計)
https://dassur.ma/things/is-postmessage-slow/
  • 另眼看 Web Worker (討論異步化編程)
https://www.ithome.com.tw/voice/132997
  • The Basics of Web Workers (2010, 談到錯誤處理和安全限制)
https://www.html5rocks.com/en/tutorials/workers/basics/
  • Blink Workers (Blink 框架 Worker 實現介紹)
https://docs.google.com/document/d/1i3IA3TG00rpQ7MKlpNFYUF6EfLcV01_Cv3IYG_DjF7M/edit#heading=h.7smox3ra3f6n
  • Should you be using Web Workers (配圖很是棒)
https://medium.com/@david.gilbertson/should-you-should-be-using-web-workers-hint-probably-not-9b6d26dc8c6a
  • How JavaScript works
https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a
  • Parallel programming in JavaScript using Web Workers
https://itnext.io/achieving-parallelism-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/

EOF

AlloyTeam 歡迎優秀的小夥伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)

clipboard.png

相關文章
相關標籤/搜索