一个看板包含十几张图表时,每个组件在挂载后独立请求数据,是最自然的写法。图表继续增加后,浏览器会同时发出大量请求,接口瞬时压力上升,关键图表反而要等待非关键请求完成。以一张 28 张图的看板为例,首屏会同时发出几十个请求:数据库和网关同时报警,主图却因为排在队尾,用户盯着白屏十几秒。
从组件视角看,每个请求都合理;从页面视角看,它们在争夺同一组资源。
页面拥有调度权
我们在数据层建立请求队列,限制同一数据源的并发数,并根据可见性和业务优先级排序。首屏核心指标先执行,视口之外的图表延后,用户切换筛选条件后,旧队列中尚未开始的任务直接取消。
正在执行的请求也要识别版本。旧条件的结果晚于新条件返回时,不能覆盖当前页面。通过查询签名与取消信号,可以避免这类竞态。
调度器不需要一开始就做成复杂框架。我用过的最小模型只有四个字段:
type QueryTask<T> = {
key: string;
priority: number;
createdAt: number;
signal: AbortSignal;
run: () => Promise<T>;
};
const MAX_CONCURRENCY = 4;
const running = new Map<string, Promise<unknown>>();
const waiting: QueryTask<unknown>[] = [];key 由数据源、时间范围、维度和筛选条件规范化生成。完全相同的任务复用 running 中的 Promise;还没开始且已经过期的任务直接从 waiting 删除。正在执行的任务通过 AbortController 尽力取消,即使底层请求无法中止,结果提交前仍要比较当前查询版本。
function effectivePriority(task: QueryTask<unknown>, now = Date.now()) {
const waitingSeconds = (now - task.createdAt) / 1000;
return task.priority + Math.min(20, waitingSeconds * 0.5);
}等待时间会逐步提高有效优先级,避免低优先级图表永久饥饿。用户刚操作的首屏查询仍然先执行,但一个已等待 40 秒的后台任务最终能获得位置。
合并不等于批量
参数完全相同的请求可以共享结果,时间范围和维度相近的请求有时可以在服务端批量处理。但盲目合并会生成超大响应,让一个失败影响所有图表。
调度策略必须可观测:排队时间、执行时间、取消原因和缓存命中都应被记录。否则优化只是在移动等待。除了指标,我还要求每次取消都带上原因字段(新任务替代、用户离开页面、过期),页面慢时能区分是服务端慢,还是请求在浏览器调度层等了很久——调度器不能从优化层变成新的黑盒。
| 指标 | 说明 | 异常意味着什么 |
|---|---|---|
queue_wait_ms |
任务等待时间 | 并发过低或请求数量失控 |
request_run_ms |
实际请求时间 | 后端或网络瓶颈 |
cancelled_before_run |
开始前取消数 | 筛选交互频繁,延迟加载有效 |
cancelled_in_flight |
执行中取消数 | 请求启动过早或响应太慢 |
dedupe_hit |
共享结果次数 | 合并策略带来的真实收益 |
queue_depth |
等待队列长度 | 页面复杂度超出调度能力 |
stale_return |
已取消但仍返回的响应 | 版本检查遗漏,最容易造成竞态 |
如果只看接口耗时,会错过任务在浏览器队列里等了两秒的事实。用户感知时间是排队、网络、转换和渲染的总和。
拿一张 28 图的看板做回归,在去重与并发限制生效后,首屏发出的请求数能降到原来的三分之一以下,P95 完成时间通常能从十几秒收敛到几秒内——其中约一半来自不再重复发起相同查询,另一半来自主图不再等辅助图表排队。这个结果印证了一件事:先把"请求在等待"这个看不见的事实变成可量化指标,再谈算法。
优先级不能永远饿死后台任务
如果核心图表持续产生新请求,低优先级任务可能一直无法执行。调度器需要老化策略:等待越久,优先级逐步提高;同一组件也要限制占用,避免单个异常图表填满队列。
用户主动操作通常高于自动刷新,但自动刷新不能悄悄覆盖用户正在查看的结果。页面可见性、交互意图和数据时效共同决定优先级,简单的先进先出或固定等级都不足以表达真实需求。
组件自治适合局部开发,页面级性能需要全局协调。架构的作用之一,就是让局部正确不会累积成整体失控。
图:调度层统一分配网络注意力,组件不再各自争抢。
调度器让页面级资源分配成为可能,但要真正省下时间和内存,还需要数据本身更紧凑、计算更早离开主线程。同一个看板场景里的后续优化,我在十万级数据渲染和把计算移进 Web Worker里分别展开:前者拆开数据管线的每个阶段,后者处理任务取消与结果过期。请求调度是这三条线里的第一层。