
本文介绍一种基于服务端流(server-send events 或 streaming response)的优化方案,让前端无需等待全部 http 请求结束,即可逐条接收并渲染已就绪的数据,显著提升响应感知速度与用户体验。
本文介绍一种基于服务端流(server-send events 或 streaming response)的优化方案,让前端无需等待全部 http 请求结束,即可逐条接收并渲染已就绪的数据,显著提升响应感知速度与用户体验。
在典型的批量数据获取场景中(例如:为 100+ 城市对并行调用天气 API),若后端采用 Promise.all() 聚合所有 fetch 结果后再统一响应,前端将被迫“阻塞等待”最慢的那个请求——即使 95% 的结果已在 200ms 内返回,用户仍需等待最后几个耗时较长的请求(可能达数秒)才能看到任何内容。
✅ 正确解法是流式响应(Streaming Response):后端不缓存全部结果,而是每完成一个子请求,就立即将其序列化为 JSON 行(NDJSON)或通过 SSE 格式写入响应流;前端则使用 ReadableStream 或 EventSource 持续读取、解析并增量更新 UI。
以下为 Node.js(Express) + React 的轻量级实现示例:
后端(Express)
HISUI 前端框架基础开发指南。HISUI 是基于 easyui 二次开发的 jQuery UI 组件库,专为医疗信息系统设计。当涉及 HISUI 组件开发、jQuery easyui 表格/表单/布局/窗口、医疗系统前端页面编写时使用此 skill。 涵盖 40 个组件的官方文档参考,支持声明式与编程式两种初始化方式,提供 $cm/$m 数据访问方法。
立即学习“前端免费学习笔记(深入)”;
app.post('/api/batch-search', async (req, res) => {
res.setHeader('Content-Type', 'application/jsonl'); // 或 'text/event-stream'
res.setHeader('Cache-Control', 'no-cache');
res.flushHeaders(); // 确保头部立即发送
const { originList, destinationList } = req.body;
for (const origin of originList) {
for (const destiny of destinationList) {
try {
const url = `https://api.example.com/route?from=${origin}&to=${destiny}`;
const resp = await fetch(url);
const data = await resp.json();
// 即时推送单条结果(NDJSON 格式:每行一个 JSON 对象)
res.write(JSON.stringify({
origin,
destiny,
text: data.text,
timestamp: Date.now()
}) + '\n');
await res.flush(); // 关键:强制刷新缓冲区
} catch (err) {
res.write(JSON.stringify({
origin,
destiny,
error: err.message,
timestamp: Date.now()
}) + '\n');
await res.flush();
}
}
}
res.end();
});前端(React)
const BatchSearch = () => {
const [results, setResults] = useState<Array<{ origin: string; destiny: string; text: string }>>([]);
useEffect(() => {
const controller = new AbortController();
fetch('/api/batch-search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ originList, destinationList }),
signal: controller.signal,
})
.then(async (response) => {
if (!response.body) throw new Error('ReadableStream not supported');
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
chunk.split('\n').forEach(line => {
if (line.trim()) {
try {
const item = JSON.parse(line);
setResults(prev => [...prev, item]);
} catch (e) {
console.warn('Invalid JSON line:', line);
}
}
});
}
})
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, []);
return (
<div>
{results.map((r, i) => (
<div key={i}>{r.origin} → {r.destiny}: {r.text}</div>
))}
</div>
);
};⚠️ 注意事项:
-
兼容性:确保 Node.js 版本 ≥ 18(原生支持
stream.Readable.from()和res.flush()),Express ≥ 4.18;前端需现代浏览器(Chrome/Firefox/Edge ≥ 2022)。 - 错误处理:流式传输中单个请求失败不应中断整个流,应单独封装错误对象并继续推送。
-
内存与背压:避免无节制推送导致前端内存溢出,可结合
request.signal或添加节流逻辑(如每 10 条批量setState)。 -
替代方案:若无法修改后端,前端可降级为「分片并发」(如每次并发 10 个请求,分批
await Promise.all()并更新状态),但体验弱于真流式。
总结:流式响应将「等待全部完成」的瀑布模型,转变为「边生产边消费」的流水线模型。它不仅降低首屏延迟(TTFB → TTI),更赋予应用实时反馈能力——这是构建高性能数据密集型前端的关键实践。















