優(yōu)化 await fetch():性能瓶頸分析與提速方案
在開發(fā) JavaScript 應(yīng)用程序時(shí),很容易忽視與 fetch() 相關(guān)的性能問題。然而,即使是像 await fetch() 這樣簡(jiǎn)單的代碼,也可能意外地拖慢你的應(yīng)用,導(dǎo)致網(wǎng)絡(luò)請(qǐng)求延遲并讓用戶感到沮喪。在本文中,我們將探討 fetch() 可能導(dǎo)致變慢的原因,并提供解決方案來修復(fù)它。
1. 冷 TCP 連接:突如其來的 200ms 延遲
癥狀:
- 對(duì) API 的第一個(gè)請(qǐng)求始終比其他請(qǐng)求花費(fèi)更長(zhǎng)時(shí)間。
- 在執(zhí)行批量操作時(shí),t??(請(qǐng)求時(shí)間的第 95 個(gè)百分位數(shù))急劇飆升。
每個(gè) fetch() 都會(huì)創(chuàng)建一個(gè)新的套接字連接,這涉及:
- DNS 查詢:將域名解析為 IP 地址。
- TCP 握手:建立 TCP 連接。
- TLS 握手:通過 HTTPS 加密連接。
在歐洲等地區(qū),平均往返時(shí)間(RTT)約為 50ms,這意味著每個(gè)新請(qǐng)求都會(huì)花費(fèi)數(shù)百毫秒的額外時(shí)間。
修復(fù)方法:
import { Agent } from'undici';
const apiAgent = newAgent({
keepAliveTimeout: 30_000,
// 保持套接字打開 30 秒
connections: 100, // 連接池大小
});
constfetchOrders = async () => {
const response = awaitfetch('https://api.payments.local/v1/orders', {
dispatcher: apiAgent
});
return response.json();
};在這段代碼中:
- apiAgent 負(fù)責(zé)管理保持連接。
- fetchOrders() 函數(shù)使用 undici 通過重用打開的連接高效地獲取數(shù)據(jù)。
2. DNS + TLS:隱藏的瓶頸
即使使用保持連接,對(duì)新域的第一個(gè)請(qǐng)求仍然會(huì)因?yàn)?DNS 查詢和 TLS 握手而變慢。
- DNS 查詢:阻塞 JavaScript 線程,在移動(dòng)網(wǎng)絡(luò)上可能長(zhǎng)達(dá) 100ms。
- TLS 握手:涉及三次往返,而 TCP 只需一次。
修復(fù)方法:
- DNS 緩存:緩存 DNS 查詢以避免重復(fù)查詢。
- 增加 maxSockets 以處理多個(gè)域。
Nginx 示例:
resolver 9.9.9.9 valid=300s;
// 緩存 DNS 響應(yīng) 300 秒Node.js 示例:
const agentWithDnsCache = newAgent({
connect: { lookup: dnsCache.lookup }, // 使用 DNS 緩存進(jìn)行更快查詢
});
constfetchFromNewDomain = async () => {
const response = awaitfetch('https://newapi.domain.com/v1/data', {
dispatcher: agentWithDnsCache,
});
return response.json();
};替代方案:QUIC/HTTP-3
為了更快地建立連接,使用支持 0-RTT 的 QUIC/HTTP-3 來繞過這些延遲。
3. response.json() 阻塞事件循環(huán)
當(dāng)服務(wù)器發(fā)送大型 JSON 響應(yīng)(5-10MB 或更大)時(shí),調(diào)用 response.json() 會(huì)阻塞事件循環(huán),消耗 100% 的 CPU。
修復(fù)方法:流式解析
import { parse } from'stream-json';
import { chain } from'stream-chain';
import { finished } from'stream';
conststreamJsonResponse = async (response) => {
const pipeline = chain([
response.body, // 來自 fetch 的 ReadableStream
parse(), // 解析 JSON 流
({ key, value }) => { /* 處理片段 */ },
]);
awaitfinished(pipeline);
};這段代碼在獲取時(shí)處理 JSON 流,避免了將大量數(shù)據(jù)一次性加載到內(nèi)存中。
4. 優(yōu)化響應(yīng)大小:壓縮和格式
如果網(wǎng)絡(luò)速度正常但加載仍然緩慢,通常是由于數(shù)據(jù)格式低效或缺乏壓縮。
修復(fù)方法:
在客戶端請(qǐng)求壓縮(例如 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 可以節(jié)省多達(dá) 25% 的空間。
5. 循環(huán)中的 await:并發(fā)殺手
當(dāng)你在循環(huán)中執(zhí)行許多異步任務(wù)時(shí),即使它們可以并發(fā)運(yùn)行,它們也會(huì)按順序執(zhí)行。
常見錯(cuò)誤:
for (const id of ids) {
const res = await fetch(`/api/item/${id}`);
items.push(await res.json());
}這會(huì)導(dǎo)致請(qǐng)求一個(gè)接一個(gè)地排隊(duì),造成延遲。
修復(fù)方法:限制并發(fā)
const maxConcurrency = 10; // 限制并發(fā)請(qǐng)求數(shù)量
const requestQueue = [...ids]; // API 請(qǐng)求的 ID 數(shù)組
const results = [];
constfetchItemsConcurrently = async () => {
awaitPromise.all(
Array.from({ length: maxConcurrency }, async () => {
while (requestQueue.length) {
const itemId = requestQueue.pop();
const response = awaitfetch(`/api/item/${itemId}`);
const itemData = await response.json();
results.push(itemData);
}
})
);
};這種方法確保只有有限數(shù)量的請(qǐng)求并發(fā)發(fā)送,防止后端過載。
6. 使用 undici.request 進(jìn)行更快的請(qǐng)求
為了提高性能,考慮使用 undici 的 request 函數(shù)而不是內(nèi)置的 fetch,因?yàn)樗烨覝p少了開銷。
import { request } from'undici';
constpostDataWithUndici = async (url, payload) => {
const { body } = awaitrequest(url, {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
});
returnawait body.json();
};7. 通過服務(wù)器配置簡(jiǎn)化 CORS 預(yù)檢
CORS 預(yù)檢請(qǐng)求增加了額外的往返延遲。簡(jiǎn)化請(qǐng)求并緩存預(yù)檢響應(yīng)有助于減少這種開銷。
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: 'https://your-frontend.com',
credentials: true,
maxAge: 86400, // 緩存預(yù)檢響應(yīng) 24 小時(shí)
}));
app.listen(3000, () => {
console.log('Server running on port 3000');
});8. HTTP/2 中的 HOL 阻塞:大文件上傳影響所有請(qǐng)求
在 HTTP/2 中,大文件上傳可能會(huì)因?yàn)閱?TCP 連接上的多路復(fù)用而阻塞其他請(qǐng)求。為了避免這種情況,應(yīng)將大請(qǐng)求和小請(qǐng)求分開。
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. 發(fā)送前的 JSON.stringify()
使用 JSON.stringify() 序列化大型負(fù)載會(huì)阻塞事件循環(huán)。相反,使用流式多部分上傳或其他高效的序列化方法。
import { Readable } from'node:stream';
conststreamPayload = async (largeData) => {
const encoder = newTextEncoder();
const stream = Readable.from(
largeData.map(item => encoder.encode(JSON.stringify(item) + '\n'))
);
awaitfetch('/bulk/ingest', { method: 'POST', body: stream });
};這種方法即使在大型數(shù)據(jù)集的情況下也能保持內(nèi)存使用量低。
結(jié)論
優(yōu)化 await fetch() 的性能涉及解決多個(gè)瓶頸。從重用連接到切換到高效的流式格式,每個(gè)修復(fù)都對(duì)更快、更高效的網(wǎng)絡(luò)請(qǐng)求做出貢獻(xiàn)??紤]使用 Undici 進(jìn)行更快的請(qǐng)求,避免使用 JSON.stringify() 進(jìn)行不必要的序列化,并實(shí)施并發(fā)控制和緩存等策略,以顯著提高應(yīng)用的性能。
持續(xù)測(cè)試,定期測(cè)量性能,并考慮使用 GraphQL-over-HTTP/2、gRPC-web 或 msgpack 等替代方案進(jìn)一步優(yōu)化你的應(yīng)用。
原文地址:https://jsdev.space/await-fetch-slow/作者:jsdev





























