背景:
在实习项目中,我遇到了一个典型的视频加载性能问题。页面上有三四个十几秒的视频,只有在满足特定条件时才会播放。最初没有做任何预加载,结果播放时偶尔会出现卡顿。于是我在 <video> 标签中添加了 rel="prefetch" 属性,但由于这些视频共用同一个标签、仅动态切换 src,实际效果并不明显。后来我尝试在组件挂载时通过 JavaScript 手动预加载,但在低速网络下首屏渲染依旧很差。经过查阅 MDN 文档并向 ChatGPT 请教,学习总结了一下预加载。(文末附本人的解决方案)
一、什么是"预加载"(Preloading)
预加载 指的是在资源还未被使用前 ,提前加载进浏览器缓存或内存 ,以便在真正需要时能瞬间可用 。
核心目标:减少用户感知的等待时间(Perceived Latency) 。
浏览器的下载队列和渲染主线程都是有限资源,预加载的设计哲学就是:
"先把带宽用在未来要用的内容上,但不要妨碍当前页面渲染。"
二、requestIdleCallback 的本质
requestIdleCallback(callback[, options])
是一个任务调度 API ,允许开发者在浏览器空闲帧期间执行非关键逻辑。
浏览器每 16.6ms(约 60FPS)会重新绘制一次。
当主线程完成布局、绘制、事件处理后,若本帧仍有空闲时间,就会执行 requestIdleCallback 注册的回调。
javascript
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 0) {
// 执行轻量任务
}
})
关键点:
- 不保证立即执行(由浏览器调度)
- 可通过
deadline.timeRemaining()判断剩余时间 - 低优先级任务(浏览器可能跳过)
- 适用于"可延迟的预加载逻辑"
换句话说:
requestIdleCallback 是**"让预加载不干扰主线程的调度器"**,不是预加载本身。
三、预加载的四种核心方式(+ 浏览器底层优先级)
| 方法 | 优先级 | 触发时机 | 适用场景 | 浏览器行为 |
<link rel="preload"> |
高 | 立即加载 | 首屏关键资源 | 主线程立刻发起请求,占带宽 |
<link rel="prefetch"> |
低 | 空闲时加载 | 可能会用到的下个页面资源 | 放入低优先级下载队列 |
JS 手动加载 (new Image(), video.load()) |
中 | 可控 | 精确控制加载逻辑 | 由 JS 主动创建请求 |
requestIdleCallback() |
调度层 | 空闲帧执行 | 调度非关键任务(如上报、缓存) | 不加载资源,仅负责"何时执行" |
四、组合优化策略(实战建议)
javascript
// 定义资源
const prefetchVideo = () => {
const link = document.createElement('link')
link.rel = 'prefetch'
link.as = 'video'
link.href = '/assets/intro.mp4'
document.head.appendChild(link)
}
// 调度执行
if ('requestIdleCallback' in window) {
requestIdleCallback(prefetchVideo)
} else {
// Safari fallback
setTimeout(prefetchVideo, 2000)
}
组合优势:
prefetch→ 利用空闲带宽;requestIdleCallback→ 避免主线程忙碌时添加<link>;- 整体节奏平衡:CPU 不阻塞,网络不浪费。
五、HTTP 层与浏览器调度机制
浏览器内部有资源优先级系统,会根据资源类型分配网络带宽:
| 类型 | 优先级 |
| HTML / CSS / JS (首屏) | Highest |
| preload | High |
| prefetch | Low |
| img / video / font (非首屏) | Medium / Low |
在 HTTP/2 / HTTP/3 下,preload 可以直接让服务端提前推送(Server Push 已废弃但仍有影响),
而 prefetch 只是提示浏览器:"这资源可能未来用到",是否加载由浏览器决定。
六、更多高级预加载技巧
- DNS Prefetch / Preconnect
提前建立 DNS 或 TCP/TLS 连接,加速外部资源。
ini
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="preconnect" href="https://cdn.example.com">
- 模块预加载(ESM 动态导入)
go
import(/* webpackPrefetch: true */ './nextPage.js')
Webpack 自动生成 <link rel="prefetch">,用于懒加载页面的提前下载。
- Service Worker 缓存预加载
可以结合 Cache API,在空闲时缓存页面资源:
ini
self.addEventListener('message', e => {
caches.open('v1').then(cache => cache.addAll(['/next.html', '/next.js']))
})
七、总结(逻辑关系图)
lua
预加载 = 提前加载 + 合理调度
└── preload:立即加载,关键资源
└── prefetch:未来资源,低优先级
└── JS手动加载:精准控制加载时机
└── requestIdleCallback:优化执行时机(CPU层)
底层机制:
- 浏览器有独立的网络调度系统;
- 各种 rel 属性影响资源优先级;
requestIdleCallback属于 任务调度层,用于协调 CPU 资源;- 组合使用时,能最大化性能与体验。
八、解决方案
ini
const preloadVideos = () => {
const videoUrls = [
//视频链接
];
videoUrls.forEach((url) => {
const link = document.createElement("link");
link.rel = "prefetch";
link.as = "video";
link.href = url;
document.head.appendChild(link);
});
};
const scheduleVideoPreload = () => {
if (typeof window === "undefined") return;
const idleCallback = (window as any).requestIdleCallback;
if (typeof idleCallback === "function") {
idleCallback(() => {
preloadVideos();
});
} else {
requestAnimationFrame(() => {
preloadVideos();
});
}
};
// 组件挂载时预加载视频
onMounted(() => {
requestAnimationFrame(() => {
scheduleVideoPreload();
});
});
有更好的解决方案欢迎评论区交流。