【第 3559 期】深入分析 await fetch() 性能问题及优化方法


src="https://api.eyabc.cn/api/picture/scenery/?k=2e9d9d6e&u=https%3A%2F%2Fmmbiz.qpic.cn%2Fsz_mmbiz_jpg%2FmeG6Vo0MevhEchribIuTYVahGlviaQOKbibDeclXobGENDViaYlcCnnT2A7MbWZWbnuWEOqUafjsTvoCkayj93FiarQ%2F0%3Fwx_fmt%3Djpeg">

前言

探讨了 JavaScript 中 await fetch() 可能导致应用变慢的原因,并提供了相应的解决方案。今日前端早读课文章由 @anliberant 分享,@飘飘编译。

译文从这开始~~

在开发 JavaScript 应用时,很多人容易忽视与 fetch() 相关的性能问题。即使是像 await fetch() 这样简单的代码,也可能会无意中拖慢应用的速度,导致网络请求延迟,让用户体验变差。本文将探讨 fetch() 导致性能下降的原因,并提供对应的解决方案。

1. 冷启动 TCP 连接:突然出现的 200ms 延迟

现象:

  • 第一次请求某个 API 总是比之后的请求慢。

  • 批量操作时,第 95 百分位的请求耗时(t₉₅)会飙升。

每次调用 fetch() 都会创建一个新的套接字连接,这包括以下几个步骤:

  • DNS 解析:将域名转换成 IP 地址

  • TCP 握手:建立 TCP 连接

  • TLS 握手:通过 HTTPS 建立安全连接

以欧洲为例,平均一个往返延迟(RTT)大约为 50 毫秒,意味着每次新连接可能多花好几百毫秒。

解决方法:

 import { Agent } from 'undici';
 const apiAgent = new Agent({
   keepAliveTimeout: 30_000,   // 保持连接 30 秒
   connections: 100,           // 连接池大小
 });
 const fetchOrders = async () => {
   const response = await fetch('https://api.payments.local/v1/orders', {
     dispatcher: apiAgent
   });
   return response.json();
 };

在此代码中:

  • apiAgent

     负责管理 Keep-Alive 的连接。

  • fetchOrders()

     使用 undici 来复用已有的连接,提高请求效率。

2. DNS 和 TLS:隐藏的性能杀手

即使使用了 Keep-Alive,第一次访问一个新域名时仍然会慢,原因是:

  • DNS 查询:会阻塞 JavaScript 线程,在移动网络下可能耗时 100 毫秒。

  • TLS 握手:相比 TCP 的一次往返,TLS 需要三次往返。

解决方法:

  • DNS 缓存:缓存 DNS 查询结果,避免重复解析。

  • 增加 maxSockets:支持同时处理多个域名。

Nginx 示例:

 resolver 9.9.9.9 valid=300s;  // 缓存 DNS 响应 300 秒

Node.js 示例:

 const agentWithDnsCache = new Agent({
   connect: { lookup: dnsCache.lookup }, // 使用 DNS 缓存加速查询
 });
 const fetchFromNewDomain = async () => {
   const response = await fetch('https://newapi.domain.com/v1/data', {
     dispatcher: agentWithDnsCache,
   });
   return response.json();
 };

替代方案:使用 QUIC/HTTP-3

借助 QUIC 或 HTTP/3 的 0-RTT 特性,可以绕过这些连接延迟,实现更快的通信。

3. response.json () 阻塞事件循环

当服务器返回大量 JSON 数据(例如 5-10MB 或更多)时,response.json() 会阻塞事件循环,占满 100% 的 CPU 资源。

解决方法:使用流式解析

 import { parse } from 'stream-json';
 import { chain } from 'stream-chain';
 import { finished } from 'stream';
 const streamJsonResponse = async (response) => {
   const pipeline = chain([
     response.body,    // fetch 返回的 ReadableStream
     parse(),          // 流式解析 JSON
     ({ key, value }) => { /* 处理每个数据片段 */ },
   ]);
   await finished(pipeline);
 };

这段代码会在数据流到达时就处理,避免一次性加载整个大文件到内存中,提高效率。

4. 优化响应体大小:压缩与数据格式

如果网络速度没问题,但加载依然缓慢,通常是因为数据格式效率底下,或者没有启用压缩。

解决方法:

前端启用压缩请求(如 Brotli):

 const fetchDataWithCompression = async (url) => {
   const response = await fetch(url, {
     headers: { 'Accept-Encoding': 'br, gzip' },
   });
   return response.json();
 };

后端(Fastify)启用 Brotli 压缩:

 const fastify = require('fastify')();
 fastify.register(require('@fastify/compress'), {
   brotliOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 } }
 });

Brotli 相比 Gzip 最多可以节省约 25% 的数据量。

5. 在循环中使用 await:并发性能杀手

在循环中执行多个异步任务时,如果使用 await,它们会按顺序一个一个执行,即使它们本可以并行完成。

常见错误:

 for (const id of ids) {
   const res = await fetch(`/api/item/${id}`);
   items.push(await res.json());
 }

这种写法会让请求一个接一个发出,严重拖慢速度。

正确写法:限制并发数量

 const maxConcurrency = 10; // 最大并发请求数
 const requestQueue = [...ids]; // API 请求 ID 队列
 const results = [];
 const fetchItemsConcurrently = async () => {
   await Promise.all(
     Array.from({ length: maxConcurrency }, async () => {
       while (requestQueue.length) {
         const itemId = requestQueue.pop();
         const response = await fetch(`/api/item/${itemId}`);
         const itemData = await response.json();
         results.push(itemData);
       }
     })
   );
 };

这样可以控制同时进行的请求数量,防止后台被瞬间请求压垮。

6. 使用 undici.request 提高请求速度

为了获得更好的性能,建议使用 undici 的 request 方法来替代内建的 fetch,因为它更快、开销更小。

 import { request } from 'undici';
 const postDataWithUndici = async (url, payload) => {
   const { body } = await request(url, {
     method: 'POST',
     body: JSON.stringify(payload),
     headers: { 'Content-Type': 'application/json' },
   });
   return await body.json();
 };

💡 fetch() 优化的适用环境说明

在本文中,大多数优化策略(如使用 undici、自定义 Agent、DNS 缓存、HTTP/2 配置)主要适用于 Node.js 环境,尤其是在服务端使用 fetch() 发起 API 请求的场景下。

而在浏览器端,由于运行环境受限:

  • 浏览器自身会进行连接复用(Keep-Alive)、DNS 缓存、TLS Session 重用等优化;

  • 不支持 undici、Node.js 的 Agent、流式 JSON 处理等接口;

  • 浏览器的 fetch() 无法手动指定连接池或 dispatcher。

因此:浏览器端建议关注的数据优化点:

  • CORS 预检减少

  • 压缩响应内容

  • 控制并发请求

  • 减少大 JSON 解析阻塞(转为分页、懒加载等)

Node.js 环境建议全面采用本文提到的所有策略,特别是高并发场景下的后端接口请求服务。

7. 通过服务器配置简化 CORS 预检请求

CORS(跨域资源共享)的预检请求会增加额外的往返延迟。简化请求方式,并缓存预检响应,可以显著降低这类开销。

 const express = require('express');
 const cors = require('cors');
 const app = express();
 app.use(cors({
   origin: 'https://your-frontend.com',  // 允许的前端域名
   credentials: true,
   maxAge: 86400,  // 缓存预检响应 24 小时
 }));
 app.listen(3000, () => {
   console.log('Server running on port 3000');
 });

8. HTTP/2 的 HOL 阻塞:大文件上传影响所有请求

在 HTTP/2 中,所有请求共用一个 TCP 连接,如果上传大文件,会因为连接阻塞(HOL Blocking)影响其他请求。为了解决这个问题,可以将大文件请求和小请求分离。

 const { Agent } = require('undici');
 const largeUploadAgent = new Agent({ maxStreamsPerConnection: 1 });
 const uploadLargeFile = async (largeFileUrl, bigFile) => {
   await fetch(largeFileUrl, { dispatcher: largeUploadAgent, body: bigFile });
 };

为了获得更好的性能,可以使用 HTTP/3,它基于 UDP,不会受到 TCP 的阻塞限制。

9. 在发送前使用 JSON.stringify () 的性能问题

对大型数据使用 JSON.stringify() 进行序列化,会阻塞事件循环。更高效的做法是使用流式 multipart 上传 或其他更轻量的序列化方法。

 import { Readable } from 'node:stream';
 const streamPayload = async (largeData) => {
   const encoder = new TextEncoder();
   const stream = Readable.from(
     largeData.map(item => encoder.encode(JSON.stringify(item) + '\n'))
   );
   await fetch('/bulk/ingest', { method: 'POST', body: stream });
 };

这种方法在处理大数据集时可以显著降低内存使用。

🧩 浏览器端 fetch 性能优化建议

尽管浏览器环境受限,仍可以从以下角度优化 fetch () 请求:

  • 避免重复请求:使用缓存策略(如 SWR、localStorage、IndexedDB)

  • 请求合并:将多个小请求合并为一个批量请求

  • 懒加载与分页:大数据集分批获取,避免阻塞主线程

  • 内容压缩:服务端开启 Gzip/Brotli,前端声明 Accept-Encoding

  • 使用 HTTP/2:确保 CDN / 服务端开启多路复用特性,浏览器自动受益

总结

优化 await fetch() 的性能,需要从多个方面着手,包括复用连接、使用高效的流式格式、限制并发数量等。使用 undici 替代默认 fetch,避免不必要的 JSON.stringify() 操作,并通过缓存和预检优化 CORS 请求,都能显著提升应用性能。

建议持续进行性能测试,定期监控并优化。可以进一步考虑使用 GraphQL-over-HTTP/2、gRPC-web 或 msgpack 等替代方案,为应用带来更出色的响应速度和效率。

关于本文
译者:@飘飘
作者:@anliberant
原文:https://jsdev.space/await-fetch-slow/

AI 前线

0 融资、10 亿美元营收,数据标注领域真正的巨头,不认为合成数据是未来

2025-12-23 22:16:26

AI 前线

Perplexity 创始人:我们不可能在资源上击败谷歌,但是有办法占据先机

2025-12-23 22:16:39

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索