懒加载实现原理与性能优化实战经验分享

保霞 框架 阅读 1,442
赞 14 收藏
二维码
手机扫码查看
反馈

懒加载的核心,Intersection Observer API

说实在的,现在做懒加载,我已经完全不用传统的 scroll + getBoundingClientRect 那套了。Intersection Observer API 在各大浏览器都支持得不错,而且性能更好,不会频繁触发重排。

懒加载实现原理与性能优化实战经验分享

我一般这样写:

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      // 预加载图片,避免闪动
      const imgLoader = new Image();
      imgLoader.onload = () => {
        img.src = img.dataset.src;
        img.classList.remove('lazy-loading');
        img.classList.add('loaded');
      };
      imgLoader.src = img.dataset.src;
      
      observer.unobserve(img); // 加载完成后取消监听
    }
  });
}, {
  rootMargin: '50px 0px', // 提前50px开始加载
  threshold: 0.01
});

// 监听所有懒加载图片
document.querySelectorAll('img[data-src]').forEach(img => {
  imageObserver.observe(img);
});

这段代码的核心就是 rootMargin 设置提前量,这样用户还没滚动到图片位置时就开始加载了。我一般设置 50px,移动端可能设得更大一点。

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

以前我犯过几个典型的错误,现在想想还是有点尴尬。

最常见的是在 scroll 事件里频繁计算:

// 错误做法:性能杀手
window.addEventListener('scroll', () => {
  const images = document.querySelectorAll('img[data-src]');
  images.forEach(img => {
    const rect = img.getBoundingClientRect();
    if (rect.top < window.innerHeight && rect.bottom > 0) {
      // 加载图片
      img.src = img.dataset.src;
    }
  });
});

这种写法会导致页面严重卡顿,因为 scroll 事件触发频率太高了。而且每次都要重新计算 DOM 位置,性能开销很大。

另一个坑是在 observe 回调里忘记取消监听:

// 错误:没有 unobserve,内存泄漏
const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.src = entry.target.dataset.src;
      // 没有 observer.unobserve(entry.target)
    }
  });
});

这样图片加载完了还在继续监听,浪费内存。正确的做法是加载完成就取消观察。

图片加载失败的处理,别让用户看到破图

实际项目中经常遇到图片地址失效的情况,这时候不能让用户看到那种破图。我一般这样处理:

const imgLoader = new Image();
imgLoader.onload = () => {
  img.src = img.dataset.src;
  img.classList.remove('lazy-loading');
  img.classList.add('loaded');
};
imgLoader.onerror = () => {
  img.src = '/images/default-placeholder.jpg'; // 替代图
  img.classList.remove('lazy-loading');
  img.classList.add('load-error');
};
imgLoader.src = img.dataset.src;

还可以加个重试机制,有时候网络抖动导致加载失败:

function loadImageWithRetry(imgElement, retryCount = 3) {
  return new Promise((resolve, reject) => {
    let attempts = 0;
    
    function attemptLoad() {
      const loader = new Image();
      loader.onload = () => resolve(loader);
      loader.onerror = () => {
        attempts++;
        if (attempts < retryCount) {
          setTimeout(attemptLoad, 1000); // 1秒后重试
        } else {
          reject(new Error('Image load failed'));
        }
      };
      loader.src = imgElement.dataset.src;
    }
    
    attemptLoad();
  });
}

视频和 iframe 的懒加载,容易被忽略

除了图片,视频和 iframe 也是资源大户。iframe 的懒加载我见过很多人不知道怎么做。

<iframe 
  data-src="https://example.com/embed" 
  width="560" 
  height="315"
  style="display: none;"
></iframe>
const iframeObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const iframe = entry.target;
      iframe.src = iframe.dataset.src;
      iframe.style.display = 'block';
      observer.unobserve(iframe);
    }
  });
});

document.querySelectorAll('iframe[data-src]').forEach(iframe => {
  iframeObserver.observe(iframe);
});

视频的懒加载类似,主要区别是要考虑预加载策略:

const videoObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const video = entry.target;
      video.preload = 'auto';
      video.load(); // 触发加载
      observer.unobserve(video);
    }
  });
});

首屏优化的小技巧

有时候页面首屏的内容也要用懒加载,比如为了更好的渐进式加载效果。这时候可以给首屏元素也加上 data-src,然后在 DOM 加载完成后立即加载首屏图片:

// DOM 加载完成后,立即加载首屏图片
document.addEventListener('DOMContentLoaded', () => {
  const firstScreenImages = Array.from(document.querySelectorAll('img[data-src]'))
    .filter(img => img.getBoundingClientRect().top < window.innerHeight);
  
  firstScreenImages.forEach(img => {
    const loader = new Image();
    loader.onload = () => {
      img.src = img.dataset.src;
      img.classList.remove('lazy-loading');
      img.classList.add('loaded');
    };
    loader.src = img.dataset.src;
  });
  
  // 其他图片继续用 Intersection Observer
  const remainingImages = document.querySelectorAll('img[data-src]:not(.loaded)');
  remainingImages.forEach(img => imageObserver.observe(img));
});

这样做可以让首屏内容更快呈现,同时保持良好的用户体验。

React/Vue 中的实际应用

在框架中使用懒加载需要注意组件销毁的问题。我一般封装成 hook:

// React hook 示例
function useLazyLoad() {
  const [observer, setObserver] = useState(null);

  useEffect(() => {
    const currentObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          const imgLoader = new Image();
          imgLoader.onload = () => {
            img.src = img.dataset.src;
            img.classList.remove('lazy-loading');
          };
          imgLoader.src = img.dataset.src;
          
          currentObserver.unobserve(img);
        }
      });
    }, {
      rootMargin: '50px 0px',
      threshold: 0.01
    });

    setObserver(currentObserver);

    return () => currentObserver.disconnect(); // 组件卸载时断开连接
  }, []);

  const observe = useCallback((element) => {
    if (element && observer) {
      observer.observe(element);
    }
  }, [observer]);

  return { observe };
}

这种封装方式保证了 observer 只创建一次,并且在组件销毁时正确清理资源。

性能监控,确保懒加载真有效果

有时候你以为做了懒加载,实际上并没有生效。我一般会加点监控代码确认效果:

let loadedCount = 0;
const totalImages = document.querySelectorAll('img[data-src]').length;

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadedCount++;
      console.log(懒加载进度: ${loadedCount}/${totalImages});
      
      if (loadedCount === totalImages) {
        console.log('所有图片加载完成');
      }
      
      // 正常的加载逻辑...
      observer.unobserve(entry.target);
    }
  });
});

当然正式上线要删掉这些 log,但开发阶段很有用,能确认懒加载是否按预期工作。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如配合 Intersection Observer 实现无限滚动等,后续会继续分享这类博客。

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

暂无评论