最近收到了好几个小白体验卡,一直在坑里,还在努力向上爬的阶段。今天摸到了一块大石头,有点硬,啃了很久,终于磕掉了一块角。下面分享这块边角料,关于 JavaScript 中的 HTTP 请求两个库 fetch
和 undici
的性能问题,保持了一点点干过点性能测试的味道。
之前学习前端知识的时候,都是选择 fetch 作为 HTTP 请求的工具。但是最近阅读了一篇文章,对比了两者的性能, undici
吞吐量大约是 fetch
的两倍。处于性能测试工作训练的敏感神经,感觉自己手动验证一下,如果确实,那后面的确是可以尝试 undici
。
在现代 JavaScript 应用中,fetch 和 Undici 是两种常见的 HTTP 客户端工具,虽然它们都用于发起网络请求,但它们的设计目标、适用场景以及性能表现有很大不同。
fetch 是浏览器端用于发起网络请求的标准 API,它的设计初衷是为了提供一个简单、统一的方式来处理 HTTP 请求和响应。在前端开发中,fetch 被广泛用于与服务器进行通信,如发起 GET、POST 请求,获取 JSON 数据,提交表单等。自 Node.js 18 版本起,fetch 也被引入到了 Node.js 中,使得在服务器端也可以使用它来进行网络请求。
fetch 的主要特点是其简洁的 API,使用 Promise 进行异步操作,这使得它非常易于上手,特别适合前端开发者。通过 fetch,开发者可以轻松发起 HTTP 请求、处理响应流和管理跨域资源请求(CORS)。然而,fetch 在某些复杂场景下的表现可能不足,例如高并发、大数据传输和长时间保持连接的需求。每次请求通常会建立新的连接,这对高性能服务器应用来说,可能会增加不必要的开销。
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 的原因主要体现在以下几个方面:
本地测试仅仅是异步串行的场景,在全异步场景和固定 limit 场景中测试还未完成,后续使用当中,若有进一步的实践,我再来写篇文章记录。
FunTester 原创精华