- 原文地址:When should you be using Web Workers?
- 原文做者:Surma
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:weibinzhu
- 校對者:ahabhgk,febrainqu
你應該在何時都使用 Web Workers。與此同時在咱們當前的框架世界中,這幾乎不可能。html
我這麼說吸引到你的注意嗎?很好。固然對於任何一個主題,都會有其精妙之處,我會將他們都展現出來。但我會有本身的觀點,而且它們很重要。繫緊你的安全帶,咱們立刻出發。前端
注意: 我討厭「新興市場」這個詞,可是爲了讓這篇博客儘量地通俗易懂,我會在這裏使用它。react
手機正變得愈來愈快。我想不會有人不一樣意。更強大的 GPU,更快而且更多的 CPU,更多的 RAM。手機正經歷與 2000 年代早期桌面計算機經歷過的同樣的快速發展時期。android
從 Geekbench 得到的基準測試分數(單核)。ios
然而,這僅僅是真實狀況的其中一個部分。低階的手機還留在 2014 年。用於製做 5 年前的芯片的流程已經變得很是便宜,以致於手機可以以大約 20 美圓的價格賣出,同時便宜的手機能吸引更廣的人羣。全世界大約有 50% 的人能接觸到網絡,同時也意味着還有大約 50% 的人沒有。然而,這些還沒上網的人也正在去上網的路上而且主要是在新興市場,那裏的人買不起有錢的西方網絡(Wealthy Western Web)的旗艦手機。git
在 Google I/O 2019 大會期間,Elizabeth Sweeny 與 Barb Palser 在一個合做夥伴會議上拿出了 Nokia 2 並鼓勵合做夥伴去使用它一個星期,去真正感覺一下這個世界上不少人平常是在用什麼級別的設備。Nokia 2 是頗有意思的,由於它看起來有一種高端手機的感受可是在外表下面它更像是一臺有着現代瀏覽器和操做系統的 5 年前的智能手機 —— 你能感覺到這份不協調。github
讓事情變得更加極端的是,功能手機正在迴歸。記得哪些沒有觸摸屏,相反有着數字鍵和十字鍵的手機嗎?是的,它們正在迴歸而且如今它們運行着一個瀏覽器。這些手機有着更弱的硬件,也許有些奇怪,卻有着更好的性能。部分緣由是它們只須要控制更少的像素。或者換另外一種說法,對比 Nodia 2,它們有更高的 CPU 性能 - 像素比。web
Nokia 8110,或者說「香蕉手機」算法
雖然咱們每一個週期都能拿到更快的旗艦手機,可是大部分人負擔不起這些手機。更便宜的手機還留在過去並有着高度波動的性能指標。在接下來的幾年裏,這些低端手機更有可能被大量的人民用來上網。最快的手機與最慢的手機之間的差距正在變大,中位數在減小。編程
手機性能的中位數在下降,全部上網用戶中使用低端手機的比例則在上升。**這不是一個真實的數據,只是爲了直觀展示。**我是根據西方世界和新興市場的人口增加數據以及對誰會擁有高端手機的猜想推斷出來的。
也許有必要解釋清楚:長時間運行的 JavaScript 的缺點就是它是阻塞的。當 JavaScript 在運行時,不能去作任何其餘事情。**除了運行一個網頁應用的 JavaScript 之外,主線程還有別的指責。**它也須要渲染頁面,及時將全部像素展現在屏幕上,而且監聽諸如點擊或者滑動這樣的用戶交互。在 JavaScript 運行的時候這些都不能發生。
瀏覽器已經對此作了一些緩解措施,例如在特定狀況下會把滾動邏輯放到不一樣的線程。不過總體而言,若是你阻塞了主線程,那麼你的用戶將會有不好的體驗。他們會憤怒地點擊你的按鈕,被卡頓的動畫與滾動所折磨。
多少的阻塞纔算過多的阻塞?RAIL 經過給不一樣的任務提供基於人類感知的時間預算來嘗試回答這個問題。好比說,爲了讓人眼感到動畫流暢,在下一幀被渲染以前你要有大約 16 毫秒的間隔。這些數字是固定的,由於人類心理學不會由於你所拿着的設備而改變。
看一下日趨擴大的性能差距。你能夠構建你的 app,作你的盡職調查以及性能分析,解決全部的瓶頸並達成全部目標。可是除非你是在最低端的手機上開發,否則是沒法預測一段代碼在現在最低端手機上要運行多久,更不要說將來的最低端手機。
這就是由不同的水平帶給 web 的負擔。你沒法預測你的 app 將會運行在什麼級別的設備上。你能夠說「Sura,這些性能低下的手機與我/個人生意無關!」,但對我來說,這如同「那些依賴屏幕閱讀器的人與我/個人生意無關!」同樣的噁心。**這是一個包容性的問題。我建議你 仔細想一想,是否正在經過不支持低端手機來排除掉某些人羣。**咱們應該努力使每個人都能獲取到這個世界的信息,而無論喜不喜歡,你的 app 正是其中的一部分。
話雖如此,因爲涉及到不少術語和背景知識,本博客沒法給全部人提供指導。上面的那些段落也同樣。我不會僞裝無障礙訪問或者給低端手機編程是一件容易的事,但我相信做爲一個工具社區和框架做者仍是有不少事情能夠去作,去以正確的方式幫助人們,讓他們的成果默認就更具無障礙性而且性能更好,默認就更加包容。
好了,嘗試從沙子開始建造城堡。嘗試去製做那些能在各類各樣的,你都沒法預測一段在代碼在上面須要運行多久的設備上都能保持符合 RAIL 模型性能評估的時間預算的 app。
一個解決阻塞的方式是「分割你的 JavaScript」或者說是「讓渡給瀏覽器」。意思是經過在代碼添加一些固定時間間隔的斷點來給瀏覽器一個暫停運行你的 JavaScript 的機會而後去渲染下一幀或者處理一個輸入事件。一旦瀏覽器完成這些工做,它就會回去執行你的代碼。這種在 web 應用上讓渡給瀏覽器的方式就是安排一個宏任務,而這能夠經過多種方式實現。
必要的閱讀: 若是你對宏任務或者宏任務與微任務的區別,我推薦你去閱讀 Jake Archibald 的談談事件循環。
在 PROXY,咱們使用一個 MessageChannel
而且使用 postMessage()
去安排一個宏任務。爲了在添加斷點以後代碼仍能保持可讀性,我強烈推薦使用 async/await
。在 PROXX 上,用戶在主界面與遊戲交互的同時,咱們在後臺生成精靈。
const { port1, port2 } = new MessageChannel();
port2.start();
export function task() {
return new Promise(resolve => {
const uid = Math.random();
port2.addEventListener("message", function f(ev) {
if (ev.data !== uid) {
return;
}
port2.removeEventListener("message", f);
resolve();
});
port1.postMessage(uid);
});
}
export async function generateTextures() {
// ...
for (let frame = 0; frame < numSprites; frame++) {
drawTexture(frame, ctx);
await task(); // 斷點
}
// ...
}
複製代碼
可是**分割依舊受到日趨擴大的性能差距的影響:**一段代碼運行到下一個斷點的時間是取決於設備的。在一臺低端手機上耗時小於 16 毫秒,但在另外一臺低端手機上也許就會耗費更多時間。
我以前說過,主線程除了執行網頁應用的 JavaScript 之外,還有別的一些職責。而這就是爲何咱們要不惜代價避免長的,阻塞的 JavaScript 在主線程。但假如說咱們把大部分的 JavaScript 移動到一條專門用來運行咱們的 JavaScript,除此以外不作別的事情的線程中呢。一條沒有其餘職責的線程。在這樣的狀況下,咱們不須要擔憂咱們的代碼受到日趨擴大的性能差距的影響,由於主線程不會收到影響,依然能處理用戶輸入並保持幀率穩定。
Web Workers,也被叫作 「Dedicated Workers」,是 JavaScript 在線程方面的嘗試。JavaScript 引擎在設計時就假設只有一條線程,所以時沒有併發訪問的 JavaScript 對象內存,而這符合全部同步機制的需求。若是一條具備共享內存模型的普通線程被添加到 JavaScript,那麼少說也是一場災難。相反,咱們有了 Web Workers,它基本上就是一個運行在另外一條獨立線程上的完整的 JavaScript 做用域,沒有任何的共享內存或者共享值。爲了使這些徹底分離而且孤立的 JavaScript 做用域能共同工做,你可使用 postMessage()
,它使你可以在另外一個 JavaScript 做用域內觸發一個 message
事件並帶有一個你提供的值的拷貝(使用結構化克隆算法 來拷貝)。
到目前爲止,除了一些一般涉及長時間運行的計算密集任務的「銀彈」用例之外 workers 基本沒獲得採用。我想這應該被改變。咱們應該開始使用 workers。常用。
這不是一個新的想法,實際上還挺老的。大部分原平生臺都把主線程稱爲 UI 線程,由於它應該只會被用來處理 UI 工做,而且它們給你提供了工具去實現。安卓從很早的版本開始就有一個叫 AsyncTask
的東西,並從那開始添加了更多更方便的 API(最近的是 Coroutines 它能夠很容易地被派發在不一樣線程)。若是你選用了「嚴格模式」,那麼在 UI 線程上使用某些 API —— 例如文件操做 —— 會致使你的應用奔潰,以此來提醒你在 UI 線程上作了一些與 UI 無關的操做。
從一開始 iOS 就有一個叫 Grand Central Dispatch (「GCD」)的東西,用來在不一樣的系統提供的線程池上派發任務,其中包括 UI 線程。經過這方式他們強制了兩個模式:你老是要將你的邏輯分割成若干任務,而後才能被放到隊列中,容許 UI 線程在須要的時候將其放入對應的線程,但同時也容許你經過簡單地將任務放到不一樣的隊列來在不一樣的線程執行非 UI 相關的工做。錦上添花的是還能夠給任務指定優先級,這樣幫助咱們確保時間敏感的工做能儘快被完成,而且不會犧牲系統總體的響應。
個人觀點是這些原平生臺從一開始就已經支持使用非 UI 線程。我以爲能夠公正地說,通過這麼多時間,他們已經證實來這是一個好主意。將在 UI 線程的工做量降到最低有助於讓你的 app 保持響應靈敏。爲何不把這樣的模式用在 web 上呢?
咱們只能經過 Web Worker 這麼一個簡陋的工具在 web 上使用線程。當你開始使用 Workers 以及他們提供的 API 時,message
事件處理器就是其中的核心。這感受並很差。此外,Workers 像線程,但又跟線程不徹底同樣。你沒法讓多個線程訪問同一個變量(例如一個靜態對象),全部的東西都要經過消息傳遞,這些消息能攜帶不少但不是所有 JavaScript 值。例如你不能發送一個 Event
或者沒有數據損失的對象實例。我想,對於開發者來講這是最大的阻礙。
由於這樣的緣由,我編寫了 Comlink 它不只幫你隱藏掉 postMessage()
,甚至能讓你忘記正在使用 Workers。感受就像是你可以訪問到來自別的線程的共享變量:
// main.js
import * as Comlink from "https://unpkg.com/comlink?module";
const worker = new Worker("worker.js");
// 這個 `state` 變量實際上是在別的 worker 中!
const state = await Comlink.wrap(worker);
await state.inc();
console.log(await state.currentCount);
複製代碼
// worker.js
import * as Comlink from "https://unpkg.com/comlink?module";
const state = {
currentCount: 0,
inc() {
this.currentCount++;
}
}
Comlink.expose(state);
複製代碼
**說明:**我用了頂層 await 以及模塊 worker(modules-in-workers)來讓例子變短。請到 Comlink 的代碼倉庫查看真實的例子以及更多細節。
在這問題上 Comlink 不是惟一的解決方案,只是我最熟悉它(很正常,考慮到是我寫的 🙄)。若是你對其餘方法感興趣,看一下 Andrea Giammarchi 的 workway 或者 Jason Miller 的 workerize。
我不在乎你用哪一個庫,只要你最終轉換到「離開主線程」架構。咱們在 PROXX 和 Squoosh 上成功使用了 Comlink,由於它很小(gzip 後 1.2KiB)而且讓咱們不須要在開發上改動太多就能使用不少來自其餘有「真正」線程的語言的經常使用模式。
最近我和 Paul Lewis 一塊兒評估過其餘的方法。除了說隱藏你正在使用 Worker 的事實以及 postMessage
,咱們還從 70 年代和使用過的參與者模式中獲得靈感,這種架構模式將消息傳遞看成基本的積木。通過那次思想實驗,咱們編寫了一個支撐參與者模式的庫,一個入門套件,並在 2018 Chrome 開發者峯會上作了一次演講,介紹了這個架構以及它的應用。
你也許會想:**是否是值得去使用「離開主線程」架構?**讓咱們來作一個投入/產出分析:有了 Comlink 這樣的庫,切換到「離開主線程」架構的代價應該會比之前有顯著的下降,很是接近於零。那麼好處呢?
Dion Almaer 叫過我去給 PROXX 寫一個徹底運行在主線程上的版本,這也許能解答那個問題。所以我就這麼作了。在 Pixel 3 或者 MacBook 上僅僅有一點可感知的差異。可是在 Nokia 2 上則有了明顯不一樣。若是把全部東西都運行在主線程上,在最差的情形下應用卡住了高達 6.6 秒。而且還有不少正在流通的設備的性能比 Nokia 2 還要低!而運行使用了「離開主線程」架構的 PROXX 版本,執行一個 tap
事件處理函數僅僅耗時 48 毫秒,由於所作的僅僅是經過調用 postMessage()
發了一條消息到 Worker 中。這表明着,特別是考慮到日趨擴大的性能差距,「離開主線程」架構可以提升處理意想不到的大且長的任務的韌性。
PROXX 的事件處理器是很是簡潔的而且只會被用來給指定的 worker 發送消息。總而言之這個任務耗時 48 毫秒。
在一個全部東西都運行在主線程的 PROXX 版本,執行一個事件處理器須要耗時超過 6 秒。
有一個須要注意的是,任務並無消失。即便使用了「離開主線程」架構,代碼仍須要運行大約 6 秒的事件(在 PROXX 這實際上會更加長)。然而因爲這些工做是在另外一個線程上進行的,UI 線程仍然能保持響應。咱們的 worker 也會把中間結果傳回主線程。經過保持事件處理器的簡潔,咱們保證了 UI 線程能保持響應並能更新視覺狀態。
如今說一下我一個脫口而出的意見:咱們現有的框架讓「離開主線程」架構變得困難並減小了它的迴歸。 UI 框架應該去作 UI 的工做,也所以有權去運行在 UI 線程。然而實際上,它們所作的工做是 UI 工做以及其餘一些相關可是非 UI 的工做。
讓咱們拿 VDOM diff 作例子:虛擬 DOM 的目的將開發者的代碼與真實 DOM 的更新解耦。虛擬 DOM 僅僅是一個模擬真實 DOM 的數據結構,這樣它的改變就不會引發高消耗的反作用。只有當框架認爲時機合適的時候,虛擬 DOM 的改變纔會引發真實 DOM 的更新。這一般被稱爲「沖洗(flushing)」。直到沖洗以前的全部工做是絕對不須要運行在 UI 線程的。然而實際上它正在耗費你寶貴的 UI 線程資源。鑑於低端手機沒法應付 diff 的工做量,在 PROXX 咱們去除了 VDOM diff 並實現了咱們本身的 DOM 操做。
VDOM diff 僅僅是其中一個框架引導的開發體驗的例子,或者一個簡單的克服用戶設備性能的例子。一個面向全球發佈的框架,除非它明確代表本身只針對哪些富有的西方網絡,不然他是有責任去幫助開發者開發支持不一樣級別手機的應用。
Web Worker 幫助你的應用運行在更普遍的設備上。像 Comlink 這樣的庫協助你在無需放棄便利以及開發速度的狀況下使用 worker。我想咱們應該思考的是,爲何除了 web 之外的全部平臺都在儘量的少佔用 UI 線程的資源。咱們應該改變本身的老辦法,並幫助促成下一代框架改變。
特別感謝 Jose Alcérreca 和 Moritz Lang,他們幫我瞭解原平生臺是如何解決相似問題的。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。