预获取技术实战:提升网页加载速度的关键策略
优化前:卡得不行
上周上线一个新功能,用户点开详情页后要加载大量数据,包括图片、用户评论、关联推荐啥的。结果测试时直接给我整无语了——首屏白屏 5 秒多,滚动还卡成幻灯片。产品经理站我身后看一眼就皱眉:“这能上线?” 我心里也清楚,再不优化,上线即事故。
其实问题早就埋下了:页面一加载就发十几个请求,有些接口响应慢,有些资源体积大,全堆在主线程里跑。用户点进去那一刻,浏览器忙得根本没空渲染,只能干等。尤其在低端机上,体验直接崩盘。
找到瓶颈了!
先别急着改代码,得知道到底卡在哪。我打开 Chrome DevTools,Network 面板一拉,时间线清清楚楚:主文档加载完之后,一堆 JS 和 API 请求排着队,关键资源(比如商品图、核心数据)被排在后面,等前面那些非关键脚本执行完才开始拉。
再切到 Performance 面板录个操作,发现 TTI(Time to Interactive)高达 4.8 秒,FCP(First Contentful Paint)也要 2.3 秒。最离谱的是,有个推荐模块的接口居然在用户滚动到那块区域前就提前发了,纯属浪费带宽和 CPU。
结论很明显:**资源加载时机太粗暴,关键路径没做优先级区分**。该提前拿的没提前,不该提前拿的反而占了坑。
试了几种方案,最后这个效果最好
一开始我想用懒加载,但懒加载解决的是“不用的时候不加载”,而我们现在的问题是“要用的时候还没加载”。所以得反过来——**预获取(prefetch / preload)** 才是正解。
我先试了 <link rel="prefetch">,结果发现它优先级太低,经常被浏览器搁置,等真要用的时候还没拉下来。后来换成 <link rel="preload">,但又遇到问题:preload 必须指定 as 类型,而且不能跨域随便用,写起来很麻烦。
折腾半天,最后决定用 **动态 import + IntersectionObserver 提前触发** 的组合拳。思路是:在用户大概率会访问下一个页面之前,或者在当前页面即将进入视口的模块前,悄悄把数据和资源拉下来,存在内存里,等真正需要时直接用,省去网络等待。
举个例子,我们有个商品详情页,下面有“看了又看”推荐区。这块区域默认在首屏以下,但统计显示 70% 的用户会滚下去看。那我就在页面加载后,用 IntersectionObserver 监听一个距离推荐区还有 300px 的“触发点”,一旦用户快滚到那儿,就提前 fetch 推荐数据。
核心代码就这几行:
// 预获取推荐数据
const prefetchRecommendations = () => {
if (window.recommendationsPrefetched) return; // 防止重复请求
window.recommendationsPrefetched = true;
fetch('https://jztheme.com/api/recommendations')
.then(res => res.json())
.then(data => {
window.prefetchedRecommendations = data; // 存到全局,后续直接用
})
.catch(err => console.warn('Prefetch failed:', err));
};
// 设置触发点
const triggerEl = document.createElement('div');
triggerEl.style.height = '1px';
document.querySelector('.product-detail').appendChild(triggerEl);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
prefetchRecommendations();
observer.unobserve(triggerEl); // 触发一次就停
}
}, { rootMargin: '300px 0px' });
observer.observe(triggerEl);
等到真正要渲染推荐区时,组件直接读 window.prefetchedRecommendations,如果存在就跳过 loading 状态:
function Recommendations() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (window.prefetchedRecommendations) {
setData(window.prefetchedRecommendations);
setLoading(false);
} else {
// 兜底:万一没预取到,正常请求
fetch('https://jztheme.com/api/recommendations')
.then(res => res.json())
.then(setData)
.finally(() => setLoading(false));
}
}, []);
if (loading) return <div>加载中...</div>;
return <div>{/* 渲染数据 */}</div>;
}
这里注意我踩过好几次坑:一定要加防重锁(window.recommendationsPrefetched),否则快速滚动可能触发多次请求;预取失败要有兜底,不能因为预取挂了就整个模块不显示。
性能数据对比
改完之后,本地测 + 真机跑,数据直接起飞:
- 首屏 FCP 从 2.3s 降到 800ms
- TTI 从 4.8s 降到 1.1s
- 推荐模块的“可见即加载完成”时间从 1.7s 降到几乎 0(因为数据已就绪)
最关键的是,用户反馈“滑下去推荐内容秒出”,不再有那种“滚着滚着突然卡住等加载”的割裂感。连产品都跑来问:“你偷偷加了什么黑科技?”
当然,也不是完美无缺。比如在弱网下,预取可能失败,但因为我们有兜底逻辑,影响不大;另外预取会稍微增加一点流量,但权衡下来,用户体验提升远大于这点成本。
其他小技巧(简单提一下)
除了动态预取,我还顺手加了几个低成本优化:
- 对首页跳转到详情页的链接,用
<link rel="prefetch">预加载关键 JS bundle(只对高频路径做) - 图片用
<img loading="lazy">+fetchpriority="high"标记首屏图 - 把非关键 CSS 拆出来,用 JS 动态注入,避免阻塞渲染
这些改动都不大,但叠加起来效果很明显。不过核心还是那个动态预取策略,它解决了“用户行为可预测性”下的资源延迟问题。
总结一下
预获取不是万能药,乱用反而浪费带宽。关键是识别用户下一步行为,在合适时机、用合适方式提前拿数据。我的经验是:优先处理“高概率、高价值、高延迟”的资源,用 IntersectionObserver 或路由跳转前的空闲期做预取,配合防重和兜底,基本就能稳住。
以上是我这次优化的真实过程,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,比如结合 service worker 做离线预缓存,后续会继续分享这类博客。

暂无评论