Web Worker 現狀

語言: CN / TW / HK

>>前端求職面試刷題神器<<

導讀:Web 是單執行緒的。這讓編寫流暢又靈敏的應用程式變得越來越困難。Web Worker 的名聲很臭,但對 Web 開發者來說,它是解決流暢度問題的 一個非常重要的工具。讓我們來了解一下 Web Worker 吧。

我們總是把 Web 和 所謂的 “Native” 平臺(比如 Android 和 iOS)放在一起比較。Web 是流式的,當你第一次開啟一個應用程式時,本地是不存在任何可用資源的。這是一個根本的區別,這使得很多在 Native 上可用的架構 無法簡單應用到 Web 上。

不過,不管你在關注在什麼領域,都一定使用過或瞭解過 多執行緒技術。iOS 允許開發者 使用 Grand Central Dispatch 簡單的並行化程式碼,而 Android 通過 新的統一任務排程器 WorkManager 實現同樣的事情,遊戲引擎 Unity 則會使用 job systems 。我上面列舉的這些平臺不僅支援了多執行緒,還讓多執行緒程式設計變得儘可能簡單。

在這篇文章,我將概述為什麼我認為多執行緒在 Web 領域很重要,然後介紹作為開發者的我們能夠使用的多執行緒原語。除此之外,我還會談論一些有關架構的話題,以此幫助你更輕鬆的實現多執行緒程式設計(甚至可以漸進實現)。

無法預測的效能問題

我們的目標是保持應用程式的 流暢(smooth)靈敏(responsive) 。流暢 意味著 穩定足夠高 的 幀率。靈敏 意味著 UI 以最低的延遲 響應 使用者互動 。兩者是保持應用程式 優雅高質量 的 關鍵因素。

按照 RAIL 模型,靈敏 意味著響應使用者行為的時間控制在 100ms 內,而 流暢 意味著螢幕上任何元素移動時 穩定在 60 fps。所以,我們作為開發者 擁有 1000ms/60 = 16.6ms 的時間 來生成每一幀,這也被稱作 “幀預算”(frame budget)。

我剛剛提到了 “我們”,但實際上是 “瀏覽器” 需要 16.6ms 的時間 去完成 渲染一幀背後的所有工作。我們開發者僅僅直接負責 瀏覽器實際工作 的一部分。瀏覽器的工作包括(但不限於):

  • 檢測 使用者操作的 元素(element)

  • 發出 對應的事件

  • 執行相關的 JavaScript 時間處理程式

  • 計算 樣式

  • 進行 佈局(layout)

  • 繪製(paint)圖層

  • 將這些圖層合併成一張 終端使用者在螢幕上看到的 圖片

  • (以及更多…)

好大的工作量啊。

另一方面,“效能差距” 在不斷擴大。旗艦手機的效能隨著 手機產品的更新換代 變得越來越高。而低端機型正在變得越來越便宜,這使得之前買不起手機的人能夠接觸到移動網際網路了。就效能而言,這些低端手機的效能相當於 2012 年的 iPhone。

為 Web 構建的應用程式會廣泛執行在效能差異很大的不同裝置上。JavaScript 執行完成的時間取決於 執行程式碼裝置有多快。不光是 JavaScript,瀏覽器執行的其他任務(如 layout 和 paint)也受制於裝置的效能。在一臺現代的 iPhone 上執行只需要 0.5ms 的任務 可能 到了 Nokia 2 上需要 10ms。使用者裝置的效能是完全無法預測的。

注:RAIL 作為一個指導框架至今已經 6 年了。你需要注意一點,實際上 60fps 只是一個佔位值,它表示的是使用者的顯示裝置原生重新整理率。例如,新的 Pixel 手機 有 90Hz 的螢幕 而 iPad Pro 的螢幕是 120Hz 的,這會讓 幀預算 分別減少到 11.1ms 和 8.3ms。

更復雜的是,除了測算 requestAnimationFrame() 回撥之間的時間,沒有更好的方法來確定執行 app 裝置的重新整理率 。

JavaScript

JavaScript 被設計成 與瀏覽器的主渲染迴圈同步執行。幾乎所有的 Web 應用程式都會遵循這種模式。這種設計的缺點是:執行緩慢的 JavaScript 程式碼會阻塞瀏覽器渲染迴圈。

JavaScript 與瀏覽器的主渲染迴圈 同步執行可以理解為:如果其中一個沒有完成,另一個就不能繼續。為了讓長時間的任務能在 JavaScript中 協調執行,一種基於 回撥 以及 後來的 Promise 的 非同步模型被建立起來。

為了保持應用程式的 流暢,你需要保證你的 JavaScript 程式碼執行 連同 瀏覽器做的其他任務(樣式、佈局、繪製…)的時間加起來不超出裝置的幀預算。

為了保持應用程式的 靈敏,你需要確保任何給定的事件處理程式不會花費超過 100ms 的時間,這樣才能及時在裝置螢幕上展示變化。在開發中,即使用自己的裝置實現上面這些已經很困難了,想要在所有的裝置都上實現這些幾乎是不可能的。

通常的建議是 “做程式碼分割(chunk your code)”,這種方式也可以被稱作 “出讓控制權(yield)給瀏覽器”。其根本的原理是一樣的:為了給瀏覽器一個時機來進入下一幀,你需要將程式碼分割成大小相似的塊(chunk),這樣一來,在程式碼塊間切換時 就能將控制權交還給 瀏覽器 來做渲染。

有很多種“出讓控制權(yield)給瀏覽器” 的方法,但是沒有那種特別優雅的。最近提出的 任務排程 API 旨在直接暴露這種能力。然而,就算我們能夠使用 await yieldToBrowser() (或者類似的其他東西) 這樣的 API 來 出讓控制權,這種技術本身還是會存在缺陷:為了保證不超出幀預算,你需要在足夠小的塊(chunk)中完成業務,而且,你的程式碼每一幀至少要 出讓一次控制權。

過於頻繁的出讓控制權 的 程式碼 會導致 排程任務的開銷過重,以至於對應用程式整體效能產生負面影響。再綜合一下我之前提到的 “無法預測的裝置效能”,我們就能得出結論 — 沒有適合所有裝置的塊(chunk)大小。當嘗試對 UI 業務進行 “程式碼分割” 時,你就會發現這種方式很成問題,因為通過出讓控制權給瀏覽器來分步渲染完整的 UI 會增加 佈局 和 繪製 的總成本。

Web Workers

有一種方法可以打破 與瀏覽器渲染執行緒同步的 程式碼執行。我們可以將一些程式碼挪到另一個不同的執行緒。一旦進入不同的執行緒,我們就可以任由 持續執行的 JavaScript 程式碼 阻塞,而不需要接受 程式碼分割 和 出讓控制權 所帶來的 複雜度 和 成本。

使用這種方法,渲染程序甚至都不會注意到另一個執行緒在執行阻塞任務。在 Web 上實現這一點的 API就是 Web Worker。通過傳入一個獨立的 JavaScript 檔案路徑 就可以 建立一個 Web Worker,而這個檔案將在新建立的執行緒里加載和執行。

const worker = new Worker("./worker.js");

在我們深入討論之前,有一點很重要,雖然 Web Workers, Service Worker 和 Worklet 很相似,但是它們完全不是一回事,它們的目的是不同的:

在這篇文章中,我只討論 Web Workers (經常簡稱為 “Worker”)。Worker 就是一個執行在 獨立執行緒裡的 JavaScript 作用域。Worker 由一個頁面生成(並所有)。

ServiceWorker 是一個 短期的 ,執行在 獨立執行緒裡的 JavaScript 作用域,作為一個 代理(proxy)處理 同源頁面中發出的所有網路請求。最重要的一點,你能通過使用 Service Worker 來實現任意的複雜快取邏輯。

除此之外,你也可以利用 Service Worker 進一步實現 後臺長請求,訊息推送 和 其他那些無需關聯特定頁面的功能。它挺像 Web Worker 的,但是不同點在於 Service Worker 有一個特定的目的 和 額外的約束。

Worklet 是一個 API 收到嚴格限制的 獨立 JavaScript 作用域,它可以選擇是否執行在獨立的執行緒上。Worklet 的重點在於,瀏覽器可以線上程間移動 Worklet。AudioWorklet,CSS Painting API 和 Animation Worklet 都是 Worklet 應用的例子。

SharedWorker 是特殊的 Web Worker,同源的多個 Tab 和 視窗可以引用同一個 SharedWorker。這個 API 幾乎不可能通過 polyfill 的方式使用,而且目前只有 Blink 實現過。所以,我不會在本文中深入介紹。

JavaScript 被設計為和瀏覽器同步執行,也就是說沒有併發需要處理,這導致很多暴露給 JavaScript 的 API 都不是 執行緒安全 的。對於一個數據結構來說,執行緒安全意味著它可以被多個執行緒並行訪問和操作,而它的 狀態(state)不會 被破壞(corrupted)。

這一般通過 互斥鎖(mutexes) 實現。當一個執行緒執行操作時,互斥鎖會鎖定其他執行緒。瀏覽器 和 JavaScript 引擎 因為不處理鎖定相關的邏輯,所以能夠做更多優化來讓程式碼執行更快。另一方面,沒有鎖機制 導致 Worker 需要執行在一個完全隔離的 JavaScript 作用域,因為任何形式的資料共享都會 因缺乏執行緒安全 而產生問題。

雖然 Worker 是 Web 的 “執行緒”原語 ,但這裡的 “執行緒” 和在 C++,Java 及其他語言中的非常不同。最大的區別在於,依賴於隔離環境 意味著 Worker 沒有許可權 訪問其建立頁面中其他變數和程式碼,反之,後者也無法訪問 Worker 中的變數。

資料通訊的唯一方式就是呼叫 API postMessage,它會將傳遞資訊複製一份,並在接收端 觸發 message 事件。隔離環境也意味著 Worker 無法訪問 DOM,在Worker 中也就無法更新 UI — 至少在沒有付出巨大努力的情況下(比如 AMP 的 worker-dom)。

瀏覽器對 Web Worker 的支援可以說是普遍的,即使是 IE10 也支援。但是,Web Worker 的使用率依舊偏低,我認為這很大程度上是由於 Worker API 特殊的設計。

JavaScript 的併發模型

想要應用 Worker ,那麼就需要對應用程式的架構進行調整。JavaScript 實際上支援兩種不同的併發模型,這兩種模型通常被歸類為 “Off-Main-Thread 架構”(脫離主執行緒架構)。

這兩種模型都會使用 Worker,但是有非常不同的使用方式,每種方式都有自己的權衡策略。這兩種模型了代表解決問題的兩個方向,而任何應用程式都能在兩者之間找到一個更合適的。

併發模型 #1:Actor

我個人傾向於將 Worker 理解為 Actor 模型 中的 Actor。程式語言 Erlang 中對於 Actor 模型 的實現可以說是最受歡迎的版本。每個 Actor 都可以選擇是否執行在獨立的執行緒上,而且完全保有自己操作的資料。沒有其他的執行緒可以訪問它,這使得像 互斥鎖 這樣的渲染同步機制就變得沒有必要了。Actor 只會將資訊傳播給其他 Actor 並 響應它們接收到的資訊。

例如,我會把 主執行緒 想象成 擁有並管理 DOM 或者說是 全部 UI 的 Actor。它負責更新 UI 和 捕獲外界輸入的事件。還會有一個 Actor 負責管理應用程式的狀態。DOM Actor 將低階的輸入事件 轉換成 應用級的語義化的事件,並將這些事件傳遞給 狀態 Actor 。

狀態 Actor 按照接收到的事件 修改 狀態物件,可能會使用一個狀態機 甚至涉及其他 Actor。一旦狀態物件被更新,狀態 Actor 就會發送一個 更新後狀態物件的拷貝 到 DOM Actor。DOM Actor 就會按照新的狀態物件更新 DOM 了。Paul Lewis 和 我 曾經在 2018 年的 Chrome 開發峰會上探索過以 Actor 為中心的應用架構 。

當然,這種模式也不是沒有問題的。例如,你傳送的每一條訊息都需要被拷貝。拷貝所花的時間不僅取決於 訊息的大小,還取決於當前應用程式的執行情況。根據我的經驗,postMessage 通常 “足夠快”,但在某些場景確實不太行。

另一個問題是,將程式碼遷移到 Worker 中可以解放 主執行緒,但同時不得不支付通訊的開銷,而且 Worker 可能會在響應你的訊息之前忙於執行其他程式碼,我們需要考慮這些問題來做一個平衡。一不小心,Worker 可能會給 UI 響應帶來負面影響。

通過 postMessage 可以傳遞非常複雜的訊息。其底層演算法(叫做 “結構化克隆”)可以處理 內部帶有迴圈的資料結構 甚至是 Map 和 Set 。然而,他不能處理 函式 或者 類,因為這些程式碼在 JavaScript 中無法跨作用域共享。

有點惱人的是,通過 postMessage 傳一個 函式 會丟擲一個 錯誤,然而一個類被傳遞的話,只會被靜默的轉換為一個普通的 JavaScript 物件,並在此過程中丟失所有方法(這背後的細節是有意義的,但是超出了本文討論的範圍)。

另外,postMessage 是一種 “Fire-and-Forget” 的訊息傳遞機制,沒有請求 和 響應 的概念。如果你想使用 請求/響應 機制(根據我的經驗,大多數應用程式架構都會最終讓你不得不這麼做),你必須自己搞定。這就是我寫了 Comlink 的原因,這是一個底層使用 RPC 協議的庫,它能幫助實現 主執行緒 和 Worker 互相訪問彼此物件。

使用 Comlink 的時候,你完全不用管 postMessage。唯一需要注意的一點是,由於 postMessage 的非同步性,函式並不會返回結果,而是會返回一個 promise。在我看來,Comlink 提煉了 Actor 模式 和 共享記憶體 兩種併發模型中優秀的部分 並 提供給使用者。

Comlink 並不是魔法,為了使用 RPC 協議 還是需要使用 postMessage。如果你的應用程式最終罕見的由於 postMessage 而產生瓶頸,那麼你可以嘗試利用 ArrayBuffers 可 被轉移(transferred) 的特性。

轉移 ArrayBuffer 幾乎是即時的,並同時完成所有權的轉移:在這個過程中 傳送方的 JavaScript 作用域會失去對資料的訪問權。當我實驗在主執行緒之外執行 WebVR 應用程式的物理模擬時,用到了這個小技巧。

併發模型 #2:共享記憶體

就像我之前提到的,傳統的執行緒處理方式是基於 共享記憶體 的。這種方式在 JavaScript 中是不可行的,因為幾乎所有的 JavaScript API 都是假定沒有併發訪問物件 來設計的。

現在要改變這一點要麼會破壞 Web,要麼會由於目前同步的必要性導致重大的效能損耗。相反,共享記憶體 這個概念目前被限制在一個專有型別:SharedArrayBuffer (或簡稱 SAB)。

SAB 就像 ArrayBuffer,是線性的記憶體塊,可以通過 Typed Array 或 DataView 來操作。如果 SAB 通過 postMessage 傳送,那麼另一端不會接收到資料的拷貝,而是收到完全相同的記憶體塊的控制代碼。在一個執行緒上的任何修改 在其他所有執行緒上都是可見的。為了讓你建立自己的 互斥鎖 和 其他的併發資料結構,Atomics 提供了各種型別的工具 來實現 一些原子操作 和 執行緒安全的等待機制。

SAB 的 缺點是多方面的。首先,也是最重要的一點,SAB 只是一塊記憶體。SAB 是一個非常低階的原語,以增加 工程複雜度 和 維護複雜度 作為成本,它提供了高靈活度 和 很多能力。而且,你無法按照你熟悉的方式去處理 JavaScript 物件 和 陣列。它只是一串位元組。

為了提升這方面的工作效率,我實驗性的寫了一個庫 buffer-backed-object。它可以合成 JavaScript 物件,將物件的值持久化到一個底層緩衝區中。

另外,WebAssembly 利用 Worker 和 SharedArrayBuffer 來支援 C++ 或 其他語言 的執行緒模型。WebAssembly 目前提供了實現 共享記憶體併發 最好的方案,但也需要你放棄 JavaScript 的很多好處(和舒適度)轉而使用另一種語言,而且通常這都會產出更多的二進位制資料。

案例研究: PROXX

在 2019 年,我和我的團隊釋出了 PROXX,這是一個基於 Web 的 掃雷遊戲,專門針對功能機。功能機的解析度很低,通常沒有觸控介面,CPU 效能差勁,也沒有湊乎的 GPU。儘管有這麼多限制,這些功能機還是很受歡迎,因為他們的售價低的離譜 而且 有一個功能完備的 Web 瀏覽器。因為功能機的流行,移動網際網路得以向那些之前負擔不起的人開放。

為了確保這款遊戲在這些功能機上靈敏流暢執行,我們使用了一種 類 Actor 的架構。主執行緒負責渲染 DOM(通過 preact,如果可用的話,還會使用 WebGL)和 捕捉 UI 事件。整個應用程式的狀態 和 遊戲邏輯 執行在一個 Worker 中,它會確認你是否踩到雷上了,如果沒有踩上,在遊戲介面上應該如何顯示。遊戲邏輯甚至會發送中間結果到 UI 執行緒 來持續為使用者提供視覺更新。

其他好處

我談論了 流暢度 和 靈敏度 的重要性,以及如何通過 Worker 來更輕鬆的實現這些目標。另外一個外在的好處就是 Web Worker 能幫助你的應用程式消耗更少的裝置電量。通過並行使用更多的 CPU 核心,CPU 會更少的使用 “高效能” 模式,總體來說會讓功耗降低。來自微軟的 David Rousset 對 Web 應用程式的功耗進行了探索。

採用 Web Worker

如果你讀到了這裡,希望你已經更好的理解了 為什麼 Worker 如此有用。那麼現在下一個顯而易見的問題就是:怎麼使用。

目前 Worker 還沒有被大規模使用,所以圍繞 Worker 也沒有太多的實踐和架構。提前判斷程式碼的哪些部分值得被遷移到 Worker 中是很困難的。我並不提倡使用某種特定的架構 而拋棄其他的,但我想跟你分享我的做法,我通過這種方式漸進的使用 Worker,並獲得了不錯的體驗:

大多數人都使用過 模組 構建應用程式,因為大多數 打包器 都會依賴 模組 執行 打包 和 程式碼分割。使用 Web Worker 構建應用程式最主要的技巧就是將 UI 相關 和 純計算邏輯 的程式碼 嚴格分離。這樣一來,必須存在於主執行緒的模組(比如呼叫了 DOM API 的)數量就能減少,你可以轉而在 Worker 中完成這些任務。

此外,儘量少的依靠同步,以便後續採用諸如 回撥 和 async/await 等非同步模式。如果實現了這一點,你就可以嘗試使用 Comlink 來將模組從主執行緒遷移到 Worker 中,並測算這麼做是否能夠提升效能。

現有的專案想要使用 Worker 的話,可能會有點棘手。花點時間仔細分析程式碼中那些部分依賴 DOM 操作 或者 只能在主執行緒呼叫的 API。如果可能的話,通過重構刪除這些依賴關係,並漸近的使用上面我提出的模型。

無論是哪種情況,一個關鍵點是,確保 Off-Main-Thread 架構 帶來的影響是可測量的。不要假設(或者估算)使用 Worker 會更快還是更慢。瀏覽器有時會以一種莫名其妙的方式工作,以至於很多優化會導致反效果。測算出具體的數字很重要,這能幫你做出一個明智的決定!

Web Worker 和 打包器(Bundler)

大多數 Web 現代開發環境都會使用打包器來顯著的提升載入效能。打包器能夠將多個 JavaScript 模組打包到一個檔案中。然而,對於 Worker,由於它建構函式的要求,我們需要讓檔案保持獨立。我發現很多人都會將 Worker 的程式碼分離並編碼成 Data URL 或 Blob URL,而不是選擇在 打包器 上下功夫來實現需求。

Data URL 和 Blob URL 這兩種方式都會帶來大問題:Data URL 在 Safari 中完全無法工作,Blob URL 雖說可以,但是沒有 源(origin) 和 路徑 的概念,這意味路徑的解析和獲取無法正常使用。這是使用 Worker 的另一個障礙,但是最近主流的打包器在處理 Worker 方面都已經加強了不少:

  • Webpack :對於 Webpack v4,worker-loader 外掛讓 Webpack 能夠理解 Worker。而從 Webpack v5 開始,Webpack 可以自動理解 Worker 的建構函式,甚至可以在 主執行緒 和 Worker 之間共享模組 而 避免重複載入。

  • Rollup :對於 Rollup,我寫過 rollup-plugin-off-main-thread ,這個外掛能讓 Worker 變得開箱即用

  • Parcel :Parcel 值得特別提一下,它的 v1 和 v2 都支援 Worker 的開箱即用,無需額外配置。

在使用這些打包器開發應用程式時,使用 ES Module 是很常見的。然而,這又會帶來新問題。

Web Worker 和 ES Module

所有的現代瀏覽器都支援通過 <script type="module" src="file.js"> 來執行 JavaScript 模組。Firefox 之外的所有現代瀏覽器現在也都支援對應 Worker 的一種寫法:new Worker("./worker.js", {type: "module"}) 。Safari 最近剛開始支援,所以考慮如何支援稍老一些的瀏覽器是很重要的。

幸運的是,所有的打包器(配合上面提到的外掛)都會確保你模組的程式碼執行在 Worker 中,即使瀏覽器不支援 Module Worker。從這個意義上來說,使用打包器可以被看作是對 Module Worker 的 polyfill。

未來

我喜歡 Actor 模式。但在 JavaScript 中的併發 設計的並不是很好。我們構建了很多的 工具 和 庫 來彌補,但終究這是 JavaScript 應該在語言層面上去完成的。一些 TC39 的工程師對這個話題很感興趣,他們正嘗試找到讓 JavaScript 更好的支援這兩種模式的方式。

目前多個相關的提案都在評估中,比如 允許程式碼被 postMessage 傳輸,比如 能夠使用 高階的,類似排程器的 API (這在 Native 上很常見) 來線上程間共享物件。

這些提案目前沒還有在 標準化流程中 取得非常重大的進展,所以我不會在這裡花時間深入討論。如果你很好奇,你可以關注 TC39 提案 ,看看下一代的 JavaScript 會包含哪些內容。

總結

Worker 是保證主執行緒 靈敏 和 流暢 的關鍵工具,它通過防止長時間執行程式碼阻塞瀏覽器渲染來保證這一點。由於和 Worker 通訊 存在 內在的非同步性,所以採用 Worker 需要對應用程式的架構進行一些調整,但作為回報,你能更輕鬆的支援各種效能差距巨大的裝置來訪問。

你應該確保使用一種 方便遷移程式碼的架構,這樣你就能 測算 非主執行緒架構 帶來的效能影響。Web Worker 的設計會導致一定的學習曲線,但是最複雜的部分可以被 Comlink 這樣的庫抽象出來。

FAQ

總會有人提出一些常見的問題和想法,所以我想先發制人,將我的答案記錄在這裡。

postMessage 不慢嗎?

我針對所有效能問題的核心建議是:先測算!在你測算之前,沒有快慢一說。但根據我的經驗,postMessage 通常已經 “足夠快” 了。這是我的一個經驗法則:如果 JSON.stringify(messagePayload) 的引數小於 10kb,即使在速度最慢的手機上,你也不用擔心會導致卡幀。如果 postMessage 真的成為了你應用程式中的瓶頸,你可以考慮下面的技巧:

  • 將你的任務拆分,這樣你就可以傳送更小的資訊

  • 如果訊息是一個狀態物件,其中只有很小一部分發生改變,那就只發送變更的部分而不是整個物件

  • 如果你傳送了很多訊息,你可以嘗試將多條訊息整合成一條

  • 最終手段,你可以嘗試將你的資訊轉化為 數字表示,並轉移ArrayBuffers 而不是 基於物件的訊息

我想從 Worker 中訪問 DOM

我收到了很多類似這樣的反饋。然而,在大多數情況下,這只是把問題轉移了。你有也許能有效地建立第二個主執行緒,但你還會遇到相同的問題,區別在於這是在不同的執行緒中。為了讓 DOM 在多執行緒中安全訪問,就需要增加鎖,這將導致 DOM 操作的速度降低,還可能會損害很多現有的 Web 應用。

另外,同步模型其實也是有優點的。它給了瀏覽器一個清晰的訊號 — 什麼時候 DOM 處於可用狀態,能夠被渲染到螢幕上。在一個多執行緒的 DOM 世界,這個訊號會丟失,我們就不得不手動處理 部分渲染的邏輯 或是 什麼其他的邏輯。

我真的不喜歡為了使用 Worker 把我的程式碼拆分成獨立的檔案

我同意。TC39 中有一些提案正在被評議,為了能夠將一個模組內聯到另一個模組中,而不會像 Data URL 和 Blob URL 一樣有那麼多小問題。雖然目前還沒有一個令人滿意的解決方案,但是未來 JavaScript 肯定會有一次迭代解決這個問題。

作者: Surma

英文 http://www.smashingmagazine.com/2021/06/web-worker s-2021

譯者: Tapir

譯文 http://zhuanlan.zhihu.com/p/393428948

寫在最後

歡迎新增poetry 微信poetries4070 我會第一時間和你分享前端行業趨勢,學習途徑,成長內容等,2022年陪你一起度過!

如果你覺得這篇內容對你有幫助,我想請你幫我3個小忙:

1.  訂閱前端面試寶典   interview2.poetries.top   讓我們成為長期關係,共同成長。

2.  訂閱程式設計導航   nav.poetries.top   整理了大量免費的程式設計學習資源。

3.  領取精心為您整理的前端面試資料

>>前端面試求職刷題神器<<