FunTester 高性能 HTTP 客户端 undici 初探

FunTester · 2024年10月09日 · 2430 次阅读

最近收到了好几个小白体验卡,一直在坑里,还在努力向上爬的阶段。今天摸到了一块大石头,有点硬,啃了很久,终于磕掉了一块角。下面分享这块边角料,关于 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 原创精华
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册