停止使用 node-fibers
由 Natalie Weizenbaum 發佈於 2021 年 3 月 26 日
我們最近收到了令人遺憾但並不完全意外的消息:node-fibers
套件已達生命週期終點,並且將不會更新以與 Node 16 相容。Dart Sass 過去一直允許 JavaScript 使用者傳入 node-fibers
來提升非同步 render()
方法的效能,但很遺憾,從現在開始,在 Node 16 及之後的版本中將不再可以使用這個選項。
有許多替代方案可以彌補效能損失,其中一些方案目前已經可以使用,一些正在開發中,還有一些方案目前只是理論上的,但可以透過像您這樣的使用者的 Pull Request 來實現。遺憾的是,目前可用的方案沒有一個能像 node-fibers
一樣方便易用,因此,如果該效能對您至關重要,我們建議您暫時繼續使用 Node 14。
發生了什麼事?發生了什麼事?永久連結
為了理解我們是如何走到這一步的,了解以下兩段歷史非常重要。首先,為什麼 Dart Sass 一開始要使用 node-fibers
?其次,為什麼 node-fibers
停止開發了?
本節內容相當技術性,如果您不關心這些細節,可以跳到下一節。
Sass 中的 FibersSass 中的 Fibers 永久連結
Dart Sass 的 JavaScript API 繼承自現已棄用的 Node Sass。這個 API 有兩個主要函式用於編譯 Sass 檔案:同步返回編譯後 CSS 的 renderSync()
,以及採用回呼函式以非同步方式傳遞編譯後 CSS 的 render()
。只有 render()
允許非同步插件,包括廣泛使用的導入器,例如 webpack 的 sass-loader
,因此 render()
在實務中被廣泛使用。
對於 Node Sass 來說,render()
和 renderSync()
之間的效能差異可以忽略不計,因為它是基於 C++ 程式碼構建的,而 C++ 程式碼在處理非同步方面幾乎沒有限制。然而,Dart Sass 作為純 JavaScript 運行,這使得它受限於 JavaScript 嚴格的非同步規則。JavaScript 中的非同步是*具有傳染性的*,這意味著如果任何函式(例如導入器插件)是非同步的,那麼所有呼叫它的函式都必須是非同步的,依此類推,直到整個程式都是非同步的。
而且 JavaScript 中的非同步操作並非沒有成本。每個非同步函式呼叫都必須分配回呼函式,將它們儲存在某處,並返回事件迴圈,然後再呼叫這些回呼函式,而這些都需要時間。事實上,它所花費的時間足以讓 Dart Sass 中的非同步 render()
比 renderSync()
慢 2-3 倍。
談談 fibers。Fibers 是一個很酷的概念,在 Ruby 和 C++ 等語言中都有提供,它讓程式設計師能更好地控制非同步函式。它們甚至允許一部分同步程式碼(例如 Sass 編譯器)呼叫非同步回呼(例如 webpack 外掛)。node-fibers
套件利用 V8 虛擬機器的某些奧秘機制在 JavaScript 中實現了 Fibers,這使得 Dart Sass 可以使用快速的同步程式碼來實現非同步的 render()
API。有一段時間,它表現得非常好。
Fibers 的消亡Fibers 的消亡 永久連結
不幸的是,node-fibers
使用的奧秘機制涉及到訪問 V8 的某些非官方公開 API 的部分。無法保證他們使用的介面在每次發布版本之間保持不變,實際上它們也確實經常改變。很長一段時間以來,這些變化很小,可以發布一個支援它們的新版 node-fibers
,但到了 Node.js 16,好運就到頭了。
最新版本的 V8 對其內部進行了一些重大修改。這些修改最終將使其能夠實現一些很酷的改進,所以很難抱怨,但副作用是 node-fibers
使用的 API 完全消失了,而且沒有明顯的替代方案。這不是任何人的錯:由於這些介面不是 V8 公開 API 的一部分,他們沒有義務保持它們的穩定性。在軟體開發中,事情有時就是這樣。
重新取得效能重新取得效能 永久連結
由於無法再將 node-fibers
傳遞給 sass.render()
,有一些選項可以恢復失去的效能。從最短期到最長期的解決方案依序為:
避免使用非同步外掛避免使用非同步外掛 永久連結
這是您可以立即執行的方法。如果可以讓您傳遞給 Sass 的外掛是同步的,則可以使用不需要 fibers 就能快速運作的 renderSync()
方法。這可能需要重寫一些現有的外掛,但它會立即帶來效益。
嵌入式 Dart Sass嵌入式 Dart Sass 永久連結
雖然它尚未準備好正式上線,但 Sass 團隊正在開發一個名為「嵌入式 Dart Sass」的專案。這涉及到將 Dart Sass 作為一個*子程序*執行,而不是一個程式庫,並使用特殊的協定與之通訊。與現有的替代方案相比,這提供了幾個重要的改進:
-
與從命令列執行
sass
不同,這仍然可以與 webpack 匯入器等外掛一起使用。事實上,我們計劃盡可能地匹配現有的 JavaScript API。這可能會讓非同步外掛的執行速度*比同步外掛更快*。 -
與現有的 JS 編譯版本不同,這將使用 Dart VM。由於 Dart 語言更具靜態特性,Dart VM 執行 Sass 的速度明顯快於 Node.js,這將為大型樣式表提供大約 2 倍的速度提升。
嵌入式 Sass 的 Node.js 主機仍在積極開發中,但如果您想試用一下,可以找到測試版本(功能極少)。
Worker ThreadsWorker Threads 永久連結
我們已經探索過在 Node.js worker thread 中運行純 JS Dart Sass 的可能性。Worker thread 的運作方式有點像 fiber,它們讓同步程式碼可以等待非同步回呼的執行。遺憾的是,它們對於可以跨執行緒邊界傳遞的資訊類型也有 *極為* 嚴格的限制,這使得使用它們來包裝像 Sass 這樣複雜的 API 變得更加困難。
目前,Sass 團隊的重點放在 Embedded Sass 上,因此我們沒有多餘的資源來深入研究 worker thread 作為替代方案。話雖如此,我們很樂意協助有興趣的使用者實作這一點。如果您有興趣,請在 GitHub issue 上跟進!
讓 node-fibers
重獲新生讓 node-fibers 重獲新生 永久連結
還有另一個可能的解決方案,儘管要將其變成現實需要真正的奉獻精神。原則上,可以向 V8 添加一個新的 API,以 *正式* 支援 node-fibers
正常運作所需的 hooks。這將使套件能夠輝煌地恢復生機,並讓 Sass 的 render()
在未來保持快速。
Sass 團隊已經與 V8 團隊和 node-fibers
的所有者聯繫,他們原則上都同意這個想法。雖然他們都沒有時間親自完成,但他們表示願意幫助願意嘗試的工程師。
不過,這並非膽小者的貢獻:它需要 C++ 知識、願意至少學習 node-fibers
程式碼庫和 V8 isolate API 的基礎知識,以及 API 設計和人際互動的技巧,以協商一個穩定的 API,既能滿足 node-fibers
的需求,又能讓 V8 團隊樂於承諾維護。但如果您有興趣,請隨時 與我們聯繫!