Prefetch实战指南:提升前端加载性能的关键技巧
首页加载慢得离谱,我决定搞点 Prefetch
上周上线一个新页面,用户反馈「首页点进去要等好几秒才出内容」。我本地 dev server 跑着飞快,一上生产环境就卡成 PPT。查了下 Network 面板,发现不是接口慢,而是用户点进首页后,才开始请求后续要用的 JS chunk 和 API 数据——等这些都加载完,黄花菜都凉了。
其实之前就知道 <link rel="prefetch"> 这玩意,但一直没真正用过,总觉得「浏览器会自动优化吧」「CDN 够快应该没问题」。结果现实狠狠打脸。这次忍不了了,动手搞 Prefetch。
一开始我以为加个标签就行,结果完全不是那么回事
我第一反应是:在首页 HTML 里加一行:
<link rel="prefetch" href="/api/user-profile" rel="external nofollow" as="fetch">
心想这下浏览器应该提前把用户资料接口缓存起来了吧?结果刷新页面,Network 里根本没看到这个请求!折腾了半天才发现,prefetch 默认只在浏览器空闲时才执行,而且很多浏览器对 as="fetch" 的支持压根不靠谱。Chrome 可能会预加载,Safari 直接忽略,Firefox 更玄学。
更坑的是,就算它真的发了请求,响应头如果没有 Cache-Control 或者 CORS 配置不对,prefetch 也会失败,但你根本看不到报错——它就静默失败了,连 console 都不给你留个话。我一度以为代码生效了,结果上线后用户照样等三秒。
后来试了下发现,手动触发才是王道
与其依赖浏览器那套不稳定的自动 prefetch,不如自己控制。我翻了下 MDN,发现可以用 fetch() 配合 { priority: 'low' }(虽然这参数还没被广泛支持),但更靠谱的做法是:**在用户可能需要资源之前,用低优先级 fetch 提前拉取并缓存**。
比如,用户打开首页后,大概率下一步会点「个人中心」。那我在首页渲染完成后,就悄悄去请求个人中心的数据和 JS bundle,存在内存或 IndexedDB 里。等用户真点进去,直接从缓存读,秒开。
但这里有个细节:不能阻塞主线程,也不能抢关键资源的带宽。所以得用 requestIdleCallback 或者简单点,用 setTimeout(fn, 0) 延迟到下一帧执行。
核心代码就这几行
最终我写了个简单的 prefetch 工具函数,专门用来预加载未来可能用到的 API 和静态资源:
const prefetchCache = new Map();
export function prefetchResource(url, options = {}) {
// 避免重复 prefetch
if (prefetchCache.has(url)) return;
// 标记已尝试 prefetch
prefetchCache.set(url, true);
// 延迟到空闲时执行
setTimeout(() => {
// 如果是 API 请求
if (url.startsWith('https://jztheme.com/api/')) {
fetch(url, {
method: 'GET',
credentials: 'include', // 带 cookie
// 注意:不要加 headers 防止触发 OPTIONS 预检
}).then(response => {
if (response.ok) {
// 把 response body 缓存起来,后面可以直接用
return response.clone().json().then(data => {
prefetchCache.set(url, data);
});
}
}).catch(err => {
// prefetch 失败无所谓,不影响主流程
console.warn('Prefetch failed for', url, err);
});
}
// 如果是 JS/CSS 资源
else if (url.endsWith('.js') || url.endsWith('.css')) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
link.as = url.endsWith('.js') ? 'script' : 'style';
document.head.appendChild(link);
}
}, 0);
}
然后在首页组件里,比如 React 的 useEffect 里调用:
useEffect(() => {
// 用户大概率会访问个人中心
prefetchResource('https://jztheme.com/api/user-profile');
// 顺便把个人中心的 JS chunk 也 prefetch 了
prefetchResource('/chunks/profile-page.js');
}, []);
等用户真点进个人中心页面时,先检查缓存:
async function loadProfile() {
const cached = prefetchCache.get('https://jztheme.com/api/user-profile');
if (cached && typeof cached === 'object') {
return cached; // 直接返回缓存数据
}
// 否则走正常请求
const res = await fetch('https://jztheme.com/api/user-profile');
return res.json();
}
实测下来,首屏到次屏的跳转时间从 1.8s 降到 0.3s,效果立竿见影。
踩坑提醒:这三点一定注意
- 别 prefetch 敏感数据:比如用户订单、隐私信息。万一用户没登录,或者权限变了,prefetch 的数据可能过期甚至泄露。我只 prefetch 公开或低敏感度数据,比如文章列表、公共配置。
- 控制数量,别贪多:我一开始 prefetch 了 5 个接口,结果首页加载反而变慢了,因为并发太多抢了主资源的带宽。现在只 prefetch 1-2 个最可能用到的。
- 缓存失效问题:上面的
prefetchCache是内存缓存,页面刷新就没了。如果要做持久化,得用 IndexedDB,但要考虑数据过期策略。目前我图省事,就用内存缓存,毕竟 prefetch 本来就是「锦上添花」,不是必须。
还有个小问题没解决,但无大碍
现在有个小瑕疵:如果用户在 prefetch 完成前就点了链接,还是会走正常请求,prefetch 的请求还在后台跑,浪费了一点流量。理论上可以 abort 掉,但我嫌麻烦就没加。反正移动端流量现在也不贵,而且概率不高。
另外,Safari 对 <link rel="prefetch"> 的支持还是不太稳定,所以我现在基本只用 JS 手动 fetch 来 prefetch API,静态资源才用 link 标签。这样兼容性更好。
总结一下
别指望浏览器自动帮你做智能 prefetch,尤其涉及 API 的时候。手动控制 + 内存缓存 + 延迟执行,这套组合拳虽然糙,但亲测有效。关键是别影响主流程,prefetch 失败了也无所谓,它只是优化,不是功能。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有人用 Service Worker 做更精细的 prefetch 控制?我试过但感觉太重了,小项目没必要。这个技巧的拓展用法还有很多,后续会继续分享这类实战博客。

暂无评论