背景
运营后台的仪表盘页面有 9 个图表组件,每个都要调后端 API。用户一进来,9 个请求同时发出,后端扛不住,前端首屏等最慢的那个 API(1.8s)。
常规方案是给每个图表加一个"进入视口再加载"。用 IntersectionObserver 实现,大概是每个组件里加一段:
const{ref,stop}=useIntersectionObserver(target,([{isIntersecting}])=>{if(isIntersecting){fetchData()stop()}})改动量不大,但有个问题:每个图表都是"看到才开始加载",用户往下滚,每个图表依次出现 loading 态。视觉上很碎,体验不好。
我需要的是另一种策略:不是"看到再加载",而是“有可能被看到之前就预加载”——这就是 quicklink 的思路,只不过它预加载的是页面 URL,我需要预加载的是 API 数据。
整体设计
参考 quicklink 的调度层架构,我搭了三个模块:
┌───────────────────────────────────┐ │ useLazyPreload(intersectionOptions)│ │ 视口检测层:哪些图表马上要进入视口 │ └───────────┬───────────────────────┘ │ 返回候选列表 ┌───────────▼───────────────────────┐ │ useIdlePreloader(threshold) │ │ 调度层:等主线程空闲后批次执行 │ └───────────┬───────────────────────┘ │ 按批次调用 ┌───────────▼───────────────────────┐ │ prefetchApi(urls, concurrency) │ │ 执行层:控制并发、去重、缓存结果 │ └───────────────────────────────────┘核心思路和 quicklink 完全一致:把"何时加载"和"如何加载"解耦。视口层只管"哪些该加载",调度层只管"什么时候执行",执行层只管"怎么加载、怎么去重"。
实现细节
第一层:视口预检测
quicklink 用IntersectionObserver检测<a>标签。我这层检测的是图表组件的容器 DOM,但不是"进入"视口才触发,而是"即将进入"视口就触发:
functionuseLazyPreload(rootMargin='300px'){constpending=ref(newSet())constobserver=newIntersectionObserver((entries)=>{entries.forEach((entry)=>{