高效能 HTTP 客戶端 undici 初探

FunTester發表於2024-10-09

最近收到了好幾個小白體驗卡,一直在坑裡,還在努力向上爬的階段。今天摸到了一塊大石頭,有點硬,啃了很久,終於磕掉了一塊角。下面分享這塊邊角料,關於 JavaScript 中的 HTTP 請求兩個庫 fetchundici 的效能問題,保持了一點點幹過點效能測試的味道。

之前學習前端知識的時候,都是選擇 fetch 作為 HTTP 請求的工具。但是最近閱讀了一篇文章,對比了兩者的效能, undici 吞吐量大約是 fetch 的兩倍。處於效能測試工作訓練的敏感神經,感覺自己手動驗證一下,如果確實,那後面的確是可以嘗試 undici

簡介 fetch 和 undici

在現代 JavaScript 應用中,fetch 和 Undici 是兩種常見的 HTTP 客戶端工具,雖然它們都用於發起網路請求,但它們的設計目標、適用場景以及效能表現有很大不同。

fetch 簡介

fetch 是瀏覽器端用於發起網路請求的標準 API,它的設計初衷是為了提供一個簡單、統一的方式來處理 HTTP 請求和響應。在前端開發中,fetch 被廣泛用於與伺服器進行通訊,如發起 GET、POST 請求,獲取 JSON 資料,提交表單等。自 Node.js 18 版本起,fetch 也被引入到了 Node.js 中,使得在伺服器端也可以使用它來進行網路請求。

fetch 的主要特點是其簡潔的 API,使用 Promise 進行非同步操作,這使得它非常易於上手,特別適合前端開發者。透過 fetch,開發者可以輕鬆發起 HTTP 請求、處理響應流和管理跨域資源請求(CORS)。然而,fetch 在某些複雜場景下的表現可能不足,例如高併發、大資料傳輸和長時間保持連線的需求。每次請求通常會建立新的連線,這對高效能伺服器應用來說,可能會增加不必要的開銷。

Undici 簡介

Undici 是專為 Node.js 設計的高效能 HTTP 客戶端,它旨在解決 Node.js 環境下高併發、高流量的網路請求需求。與 fetch 相比,Undici 更專注於效能最佳化,特別是在伺服器端應用場景中。它的名字來源於義大利語,意指 “十一”(即 Node.js 的 HTTP 標準庫 http 是第 11 號 RFC 提案)。

Undici 的核心優勢在於其高效的連線管理。它內建了連線池,可以複用 HTTP 連線,避免了每次請求都重新建立連線的開銷,尤其適用於需要頻繁發起網路請求的高併發應用。此外,Undici 還完全支援 HTTP/1.1 和 HTTP/2 協議,在流處理方面表現優秀,能夠有效處理大型資料傳輸或流式響應。它的錯誤處理機制也比 fetch 更加完善,提供了自動重試功能,減少了手動處理錯誤的複雜性。

兩者對比

fetch 和 Undici 的主要區別在於適用場景和效能表現。fetch 是一個通用的 HTTP 客戶端,適用於瀏覽器環境和簡單的伺服器請求,而 Undici 則專為高效能、高併發的 Node.js 伺服器應用設計。Undici 透過高效的連線池和流處理,顯著提升了在複雜伺服器場景中的效能,是需要最佳化伺服器效能時的理想選擇。

這是關於 fetch 和 Undici 的詳細對比表格,涵蓋了它們的特性、效能和適用場景。以下是完整的表格:

特性 fetch Undici
適用環境 主要用於瀏覽器環境;Node.js 18+ 支援 專為 Node.js 設計,適用於伺服器端應用
設計目標 通用的 HTTP 客戶端,用於簡單網路請求 高效能、低開銷的 HTTP 客戶端,專注高併發和效能
效能 效能適中,適合小型或普通請求 高效能,尤其適用於高併發和大量請求場景
連線管理 每次請求可能建立新連線(根據 HTTP 版本) 內建連線池,支援連線複用,大幅提升效率
非同步支援 原生支援 Promise 非同步處理 最佳化非同步效能,使用現代 JavaScript 非同步特性
流處理 支援透過 ReadableStream 處理流式響應 高效支援流式請求與響應處理,適合大型資料傳輸
錯誤處理 需要手動處理錯誤(如網路錯誤、狀態碼等) 提供內建的錯誤處理機制,支援自動重試
請求攔截 透過 AbortController 可以中斷請求 提供內建的攔截機制,允許更復雜的請求控制
HTTP/2 支援 不支援 HTTP/2 完全支援 HTTP/1.1 和 HTTP/2,且管理更高效
檔案上傳 支援透過 FormData 進行檔案上傳 高效處理檔案上傳和大資料請求
API 複雜度 API 簡單易用,語法簡潔 API 強大,提供豐富的配置選項和功能
依賴性 無需額外依賴,Node.js 原生支援 需透過 npm 安裝,可靈活升級和擴充套件
適用場景 適合簡單、通用的 HTTP 請求,尤其是瀏覽器端應用 適合高效能、高併發的伺服器端應用和微服務架構
擴充套件性 通用性強,但在複雜場景下需自定義封裝 擴充套件性強,能處理複雜的請求需求
易用性 簡單易用,適合前端開發者 學習曲線稍陡峭,但效能最佳化效果顯著
支援特性 內建 CORS 支援,適用於跨域請求 專注效能最佳化和資源管理,適合高負載應用場景
社群支援 瀏覽器 API,廣泛支援,文件豐富 Node.js 團隊維護,逐漸被更多專案採用

簡單測試

下面是複用了兩者在效能測試的差異的用例,因為大多數程式碼都是一致的,我把註釋掉的程式碼也一起附上了。

import {request} from 'undici'; //使用undici  


//獲取當前時間  
let start = new Date().getTime();  
for (let i = 0; i < 100000; i++) {  
//     await request('http://localhost:8080').then((response) => {  
//         response.body.text();  
//     });  
    await fetch('http://localhost:8080').then((response) => {  
        response.text();  
    });  
}  

let number = new Date().getTime() - start;  
console.log("cost time :", number / 1000);

我在本地啟動了一個 HTTP 伺服器,直接返回了響應,之前測試過 TPS 可以達到 10 萬 QPS 以上的效能,足夠滿足本次的測試。

由於還未掌握 JavaScript 效能基準測試技能,還是使用原始的計時來表示效能搞低。 fetch 的時間約 8s ,而 undici 的時間約 4.2s ,四捨五入一下,也算是提升兩倍了。

undici 原始碼裡面還有一個 stream 方法,據悉是更快版本的 request 方法,測試了一下,實在沒看出來差異。估計是我用法不對。下面是原始碼:

/ A faster version of `request`. */  
declare function stream(  
  url: string | URL | UrlObject,  
  options: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions, 'origin' | 'path'>,  
  factory: Dispatcher.StreamFactory  
): Promise<Dispatcher.StreamData>;

追本溯源

Undici 的效能通常高於原生 fetch 的原因主要體現在以下幾個方面:

連線管理

  • 連線複用:Undici 內建了連線池機制,可以有效複用 TCP 連線,減少了每次請求時建立和關閉連線的開銷。這種複用在高併發場景下表現尤為突出。
  • 更高效的併發處理:在處理大量併發請求時,Undici 的連線管理策略能顯著降低延遲,從而提高整體效能。

效能最佳化

  • 精簡的底層實現:Undici 的實現專注於效能最佳化,使用了更少的抽象層和額外的功能,這使得請求的處理更高效。相比之下,fetch 可能會受到其他功能的影響。
  • 專為 Node.js 設計:Undici 是專門為 Node.js 環境開發的,充分利用了 Node.js 的非同步特性,使得它在這方面的表現更優。
  • 減少不必要的中間層:fetch 是一個較為通用的 API,旨在支援多種環境(如瀏覽器和 Node.js),可能引入了一些額外的開銷。而 Undici 的設計目標是專注於 Node.js 伺服器環境,因此能夠減少一些通用性帶來的效能損失。

本地測試僅僅是非同步序列的場景,在全非同步場景和固定 limit 場景中測試還未完成,後續使用當中,若有進一步的實踐,我再來寫篇文章記錄。

FunTester 原創精華
  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章