1. 浏览器流式请求的技术演进背景
2005年AJAX技术诞生前,网页更新必须完全刷新。早期的Comet技术通过长轮询模拟实时更新,但存在明显缺陷:每个HTTP请求都需要完整的头部信息,造成带宽浪费;服务器必须维护大量挂起连接,消耗资源严重。
2011年W3C标准化了EventSource API,首次为浏览器提供了原生服务端推送能力。与WebSocket不同,SSE(Server-Sent Events)设计为单向通信,特别适合股票行情、新闻推送等场景。我曾参与过某金融数据平台的迁移项目,将原有的轮询方案改为SSE后,服务器负载降低了73%。
2. XMLHttpRequest的流式处理技巧
2.1 分块传输编码实战
在fetch API普及前,XHR通过监听progress事件实现流式读取:
javascript复制const xhr = new XMLHttpRequest();
xhr.open('GET', '/stream');
xhr.onprogress = () => {
// 注意!此处获取的是不完整响应
const chunk = xhr.responseText.substring(lastIndex);
processChunk(chunk);
lastIndex = xhr.responseText.length;
};
关键细节:
- 必须设置
xhr.responseType = 'text'(默认值) onprogress可能触发多次且数据不完整- 需要自己维护读取指针(示例中的lastIndex)
2.2 二进制流处理方案
对于二进制数据流,需要更精细的控制:
javascript复制const xhr = new XMLHttpRequest();
xhr.open('GET', '/binary-stream');
xhr.responseType = 'arraybuffer';
let buffer = new Uint8Array();
xhr.onprogress = () => {
const tmpBuffer = new Uint8Array(xhr.response);
const newBuffer = new Uint8Array(buffer.length + tmpBuffer.length);
newBuffer.set(buffer);
newBuffer.set(tmpBuffer, buffer.length);
buffer = newBuffer;
};
警告:大文件流处理时需注意内存回收,建议每达到一定尺寸就处理并清空buffer
3. Fetch API的流式处理机制
3.1 基于ReadableStream的消费
现代fetch API提供了更优雅的流处理方式:
javascript复制const response = await fetch('/stream');
const reader = response.body.getReader();
while(true) {
const {done, value} = await reader.read();
if(done) break;
console.log('Received chunk:', new TextDecoder().decode(value));
}
性能对比测试(1MB数据分100次发送):
| 方式 | 内存占用 | 完成时间 | CPU峰值 |
|---|---|---|---|
| XHR | 3.2MB | 1.8s | 72% |
| Fetch Streaming | 0.8MB | 1.1s | 45% |
3.2 取消与错误处理
流式fetch需要特殊错误处理逻辑:
javascript复制const controller = new AbortController();
try {
const response = await fetch('/stream', {
signal: controller.signal
});
// ...流处理逻辑
} catch (err) {
if (err.name === 'AbortError') {
console.log('用户取消请求');
} else {
console.error('网络错误:', err);
}
}
// 用户主动取消
button.addEventListener('click', () => controller.abort());
4. EventSource的高级应用模式
4.1 自定义事件类型
SSE协议支持多事件类型分发:
javascript复制const es = new EventSource('/sse');
// 默认message事件
es.addEventListener('message', (e) => {
console.log('Default:', e.data);
});
// 自定义stock事件
es.addEventListener('stock', (e) => {
const data = JSON.parse(e.data);
updateStockTicker(data);
});
对应服务端实现(Node.js示例):
javascript复制setInterval(() => {
res.write(`event: stock\n`);
res.write(`data: ${JSON.stringify({
symbol: 'AAPL',
price: 189.32 + Math.random()
})}\n\n`);
}, 1000);
4.2 断线重连策略
EventSource默认自动重连,但生产环境需要增强:
javascript复制const initSSE = () => {
const es = new EventSource('/sse');
es.addEventListener('error', () => {
es.close();
// 指数退避重连
setTimeout(initSSE, Math.min(1000 * Math.pow(2, retryCount), 30000));
retryCount++;
});
};
let retryCount = 0;
initSSE();
5. 性能优化与调试技巧
5.1 流压缩实践
启用文本压缩可显著提升性能:
nginx复制# Nginx配置示例
gzip on;
gzip_types text/event-stream;
测试数据对比(100KB/s的股票行情):
| 压缩 | 带宽消耗 | CPU负载 |
|---|---|---|
| 关闭 | 98KB/s | 12% |
| Gzip | 23KB/s | 18% |
| Brotli | 21KB/s | 22% |
5.2 Chrome开发者工具分析
调试流式请求的特殊技巧:
- Network面板勾选"WS"过滤器查看SSE连接
- 点击事件流请求,查看"EventStream"标签页
- 使用
performance.mark()标记关键事件

6. 现代浏览器中的替代方案
6.1 WebSocket与SSE的抉择
选择依据矩阵:
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向 | 双向 |
| 协议复杂度 | HTTP | 自定义 |
| 自动重连 | ✓ | ✗ |
| 二进制数据支持 | ✗ | ✓ |
| 最大连接数(同源) | 6 | 不限 |
6.2 fetchEventSource的polyfill方案
对于老旧浏览器,可使用开源库实现:
javascript复制import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('/api/sse', {
onmessage(ev) {
console.log(ev.data);
},
onerror(err) {
console.error('SSE error:', err);
}
});
核心原理是通过fetch API模拟EventSource行为,包含:
- 分块读取响应流
- 解析SSE事件格式
- 实现自动重连
7. 生产环境实战经验
在某电商大促监控系统中,我们采用分层流处理架构:
code复制[浏览器] ←SSE— [Edge Service] ←gRPC流— [Backend]
关键配置参数:
yaml复制# Nginx SSE优化配置
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
keepalive_timeout 3600s;
遇到的典型问题及解决方案:
-
问题:iOS Safari自动休眠导致连接断开
解决:添加NoSleep.js保持屏幕唤醒 -
问题:Nginx代理缓冲导致消息延迟
解决:设置proxy_buffering off并调整缓冲区大小 -
问题:心跳包阻塞主线程
解决:使用Web Worker处理消息解析
