Prefetch实战指南:提升前端加载性能的关键技巧

歆艺 ☘︎ 优化 阅读 2,845
赞 57 收藏
二维码
手机扫码查看
反馈

首页加载慢得离谱,我决定搞点 Prefetch

上周上线一个新页面,用户反馈「首页点进去要等好几秒才出内容」。我本地 dev server 跑着飞快,一上生产环境就卡成 PPT。查了下 Network 面板,发现不是接口慢,而是用户点进首页后,才开始请求后续要用的 JS chunk 和 API 数据——等这些都加载完,黄花菜都凉了。

Prefetch实战指南:提升前端加载性能的关键技巧

其实之前就知道 <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 控制?我试过但感觉太重了,小项目没必要。这个技巧的拓展用法还有很多,后续会继续分享这类实战博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论