setInterval定时请求延迟:未完成请求状态与丢帧实现方案
高频HTTP请求更新Canvas的问题与解决方案
需求背景
需要每秒10次通过HTTP请求获取数据,更新<canvas>中的图像,原实现代码如下:
var i = 0; setInterval(() => { i += 1; console.log("sending request", i); fetch("/data") // 该请求耗时有时会超过100ms .then((r) => r.arrayBuffer()) .then((arr) => { // 通过putImageData更新canvas... console.log("finished processing", i); }); }, 100);
实际运行问题
由于请求耗时偶尔超过100ms,出现了如下日志现象:
sending request 1 sending request 2 sending request 3 finished processing 3 sending request 4 sending request 5 finished processing 5 sending request 6 sending request 7 sending request 8
技术问询与解答
1. 未完成的请求是会排队导致后续更慢,还是直接被丢弃?
未完成的请求不会被自动丢弃,浏览器会并行处理多个同源HTTP请求(一般Chrome限制为6个并发连接)。但如果请求持续堆积超过并发限制,后续新请求会进入等待队列,占用网络连接池资源,导致整体响应变慢;同时所有已发出的请求返回后,对应的回调都会执行,就像日志里看到的请求1、2先发出,但请求3先完成,说明三个请求是并行处理的,只是返回顺序不同。
2. 如何修改示例代码来验证上述两种情况?
验证「请求堆积导致变慢」
给每个请求添加时间戳和并发数统计,观察请求耗时和活跃请求数的变化:
let i = 0; let activeRequests = 0; setInterval(() => { i += 1; const sendTime = Date.now(); console.log(`[${sendTime}] 发送请求 ${i}, 活跃请求数: ${++activeRequests}`); fetch("/data") .then((r) => r.arrayBuffer()) .then((arr) => { const finishTime = Date.now(); console.log(`[${finishTime}] 完成请求 ${i}, 耗时: ${finishTime - sendTime}ms, 活跃请求数: ${--activeRequests}`); }) .catch(() => { --activeRequests; }); }, 100);
如果日志中活跃请求数持续增长,且后续请求的耗时越来越长,说明请求在堆积,导致后续请求等待空闲连接而变慢。
验证「请求是否被丢弃」
让服务器给每个请求返回唯一标识,前端对比发送的请求序号和返回的标识:
修改Python服务器代码:
import bottle, time from itertools import count app = bottle.Bottle() req_counter = count(1) @bottle.route('/') def index(): return open("test.html", "r").read() @bottle.route('/data') def data(): time.sleep(0.150) req_id = next(req_counter) return f"{req_id}".encode() bottle.run(port=80)
修改前端代码:
let i = 0; setInterval(() => { i += 1; console.log("发送请求", i); fetch("/data") .then((r) => r.text()) .then((reqId) => { console.log(`完成请求 ${reqId}, 发送时序号为 ${i}`); }); }, 100);
如果返回的reqId是连续的,说明所有发出的请求都得到了响应,没有被丢弃。
3. 针对视频场景,若服务器响应过慢需丢弃帧,该如何实现?
视频场景需要优先处理最新帧,可通过以下两种方式实现:
方式1:用AbortController取消旧请求
每次发起新请求前,取消上一个未完成的请求,节省网络资源:
let i = 0; let abortController = null; function fetchAndUpdate() { i += 1; // 取消上一个未完成的请求 if (abortController) { abortController.abort(); } abortController = new AbortController(); console.log("发送请求", i); fetch("/data", { signal: abortController.signal }) .then((r) => { if (r.ok) return r.arrayBuffer(); throw new Error("请求失败"); }) .then((arr) => { console.log("完成请求", i); // 更新canvas... }) .catch((err) => { if (err.name !== "AbortError") { console.error("请求错误:", err); } }); } // 用setTimeout替代setInterval,避免请求堆积(可选) function scheduleNext() { fetchAndUpdate(); setTimeout(scheduleNext, 100); } scheduleNext();
方式2:标记最新请求,忽略旧响应
不取消请求,仅在回调中判断当前响应是否为最新帧:
let i = 0; let latestRequestId = 0; setInterval(() => { const currentId = ++i; latestRequestId = currentId; console.log("发送请求", currentId); fetch("/data") .then((r) => r.arrayBuffer()) .then((arr) => { // 如果当前响应不是最新的,直接丢弃 if (currentId !== latestRequestId) { console.log("丢弃旧帧", currentId); return; } console.log("完成请求", currentId); // 更新canvas... }); }, 100);
内容的提问来源于stack exchange,提问作者Basj
相关产品推荐
相关产品推荐

