前端预获取技术实战指南从DNS到资源缓存的性能优化

迷人的圣贤 优化 阅读 1,618
赞 8 收藏
二维码
手机扫码查看
反馈

我的预获取策略,经过多个项目验证

说实话,预获取这玩意儿我一直觉得挺玄学的,直到去年重构那个电商项目才真正搞明白怎么玩。之前总是觉得预获取就是简单地提前加载资源,结果经常遇到缓存污染或者浪费带宽的问题。

前端预获取技术实战指南从DNS到资源缓存的性能优化

我现在的做法是在路由切换前进行智能预获取,不是盲目的全量预取。比如用户在商品列表页停留超过2秒,我就开始预获取详情页可能用到的数据:

// 路由守卫中的预获取逻辑
const prefetchData = (to, from) => {
  if (from.name === 'product-list' && to.name === 'product-detail') {
    // 获取商品基本信息
    fetch('https://jztheme.com/api/product-basic', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id: to.params.id })
    }).then(res => res.json()).then(data => {
      // 存入临时缓存
      window.__PRELOAD_CACHE__ = data;
    });
    
    // 同时预拉取相关资源
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = '/api/product-detail/' + to.params.id;
    document.head.appendChild(link);
  }
};

这样处理的好处很明显:用户点击后响应速度快,而且不会因为误操作产生过多无效请求。不过这里有个坑要注意,预获取的数据要设置合适的过期时间,不然缓存数据和实时数据不一致会导致问题。

这几种错误写法,别再踩坑了

最常见的错误就是在页面加载的时候一次性预获取所有可能用到的资源。我见过不少项目这么干,结果页面首屏加载巨慢无比。比如这个典型的错误写法:

// 错误做法:一次性预获取所有资源
window.addEventListener('load', () => {
  // 一口气预获取十几个页面的数据
  for (let i = 1; i <= 20; i++) {
    fetch(/api/page-${i}.json);
  }
});

这种写法简直是性能杀手,用户还没决定要去哪个页面,你就把所有数据都拉下来了。还有个常见错误是不区分资源优先级,图片、视频、API都用同一个预获取策略。我曾经就在一个视频项目里犯过这个错误,结果低端设备直接卡死。

另一个容易出错的地方是忘记处理错误情况。很多人只考虑成功场景,但网络异常、服务器错误这些情况也得有fallback机制:

// 错误处理不能少
const safePrefetch = async (url) => {
  try {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 5000); // 5秒超时
    
    const response = await fetch(url, {
      signal: controller.signal,
      priority: 'low'
    });
    
    if (!response.ok) {
      console.warn('预获取失败:', url);
      return null;
    }
    
    return response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.warn('预获取超时:', url);
    } else {
      console.error('预获取错误:', error);
    }
    return null;
  }
};

实际项目中的坑

在移动端项目中最容易出问题的是内存管理。预获取的数据如果处理不当,很容易导致内存泄漏。我之前在一个混合APP项目里就遇到过这个问题,预获取的商品图片太多,结果老设备直接崩溃。

我的解决方案是建立一个简单的LRU缓存机制,控制预获取数据的最大数量:

class PrefetchCache {
  constructor(maxSize = 10) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }
  
  set(key, value) {
    if (this.cache.size >= this.maxSize) {
      // 删除最早添加的项
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    this.cache.set(key, value);
  }
  
  get(key) {
    return this.cache.get(key);
  }
  
  has(key) {
    return this.cache.has(key);
  }
}

const prefetchCache = new PrefetchCache(8);

还有个问题是并发控制。预获取太多请求同时发起,不仅会影响当前页面的性能,还可能触发服务端的频率限制。所以我一般会限制并发数量:

class ConcurrentPrefetch {
  constructor(maxConcurrent = 3) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
  }
  
  async add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.process();
    });
  }
  
  async process() {
    if (this.running >= this.maxConcurrent || this.queue.length === 0) {
      return;
    }
    
    this.running++;
    const { task, resolve, reject } = this.queue.shift();
    
    try {
      const result = await task();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.process(); // 继续处理队列
    }
  }
}

const concurrentPrefetch = new ConcurrentPrefetch(3);

最后提一下用户体验方面的问题。预获取虽然能提升性能,但如果处理不当会让用户感觉奇怪。比如在表单页面预获取提交后可能用到的数据,但用户最终没有提交表单,这就造成了资源浪费。所以我在实际项目中会结合用户行为分析来判断是否需要预获取。

性能监控不能忽视

预获取的效果需要通过数据来验证,光靠感觉是不行的。我在项目中加入了一些简单的监控代码:

const prefetchMetrics = {
  startTime: {},
  trackStart: (key) => {
    prefetchMetrics.startTime[key] = performance.now();
  },
  trackEnd: (key) => {
    if (prefetchMetrics.startTime[key]) {
      const duration = performance.now() - prefetchMetrics.startTime[key];
      console.log(预获取 ${key} 耗时: ${duration}ms);
      
      // 上报到监控系统
      if (window.gtag) {
        gtag('event', 'prefetch_duration', {
          event_category: 'performance',
          value: Math.round(duration)
        });
      }
    }
  }
};

// 使用示例
prefetchMetrics.trackStart('product_detail_' + productId);
safePrefetch(/api/product/${productId}).then(data => {
  prefetchMetrics.trackEnd('product_detail_' + productId);
  // 处理数据...
});

通过这些监控数据,我发现某些页面的预获取成功率并不高,后来调整了预获取时机,效果明显改善。所以建议大家一定要加上监控,不然预获取做得好不好完全没概念。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。如果有更优的实现方式欢迎评论区交流。

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

暂无评论