如何实现一个图片懒加载功能,并考虑性能优化?

各位老铁,双击屏幕,今天咱们就来聊聊前端性能优化里的一大利器——图片懒加载。 啥? 你说图片懒加载听起来很高大上? 其实啊, 就是咱们让那些暂时看不见的图片先别着急加载, 等它们滚到视窗里了再露脸, 这样就能减轻页面初始加载的负担, 让用户更快地看到内容, 体验嗖嗖地往上涨!

一、 为什么需要图片懒加载?

想象一下,如果你的页面有几百张图片,而且用户只看了最上面的几张,剩下的图片是不是白白浪费了带宽? 这种行为简直是“带宽刺客”! 尤其是在移动端, 流量可是金钱啊!

懒加载的意义就在于:

  • 提升页面加载速度: 减少首次加载时HTTP请求的数量,加快页面渲染。
  • 节省带宽: 只加载用户可见区域的图片,避免浪费流量。
  • 提升用户体验: 更快的加载速度意味着更流畅的体验,谁不喜欢呢?

二、 懒加载的实现方案

实现懒加载的方法有很多,咱们从最基础的开始, 逐步深入。

1. 传统方案: offsetTop + window.innerHeight + window.pageYOffset

这是最原始,也是兼容性最好的一种方案。 它的核心思想是: 判断图片是否进入了可视区域。

  • offsetTop: 图片距离文档顶部的距离。
  • window.innerHeight: 浏览器视窗的高度。
  • window.pageYOffset: 页面垂直滚动的距离。

只要 offsetTop 小于 window.innerHeight + window.pageYOffset, 就说明图片进入了可视区域。

代码示例:

<!DOCTYPE html>
<html>
<head>
<title>传统懒加载</title>
<style>
  img {
    display: block; /* 确保图片是块级元素,offsetTop才能正确计算 */
    margin-bottom: 20px; /* 为了方便演示,图片之间加点间距 */
  }
</style>
</head>
<body>

  <img data-src="image1.jpg" src="placeholder.gif" alt="Image 1">
  <img data-src="image2.jpg" src="placeholder.gif" alt="Image 2">
  <img data-src="image3.jpg" src="placeholder.gif" alt="Image 3">
  <!-- 更多图片... -->

  <script>
    function lazyLoad() {
      const images = document.querySelectorAll('img[data-src]'); // 获取所有带有data-src属性的img标签
      const windowHeight = window.innerHeight;
      const scrollY = window.pageYOffset;

      images.forEach(img => {
        if (img.offsetTop < windowHeight + scrollY) {
          img.src = img.dataset.src; // 替换src
          img.removeAttribute('data-src'); // 移除data-src,避免重复加载
        }
      });
    }

    // 页面加载完成时执行一次,防止首屏图片未加载
    window.addEventListener('load', lazyLoad);
    // 监听滚动事件
    window.addEventListener('scroll', lazyLoad);
  </script>

</body>
</html>
  • data-src: 我们使用 data-src 属性来存储图片的真实地址, src 属性先放一张占位图 placeholder.gif
  • lazyLoad 函数: 这个函数负责判断图片是否进入可视区域,如果进入了,就把 data-src 的值赋给 src, 并移除 data-src 属性。
  • 事件监听: 监听 loadscroll 事件, 确保在页面加载完成和滚动时都执行 lazyLoad 函数。

这个方案的优点是兼容性好,缺点是性能较差,每次滚动都会遍历所有图片,即使图片已经加载过。

2. 优化方案: 函数节流(Throttling)

为了解决传统方案的性能问题, 我们可以使用函数节流。 函数节流是指: 在一定时间内, 只能执行一次函数。 这样可以减少 lazyLoad 函数的执行频率。

代码示例:

function throttle(func, delay) {
  let timeoutId;
  let lastExecTime = 0;

  return function(...args) {
    const now = Date.now();

    if (!timeoutId) {
      // 第一次立即执行
      func.apply(this, args);
      lastExecTime = now;
      timeoutId = setTimeout(() => {
        timeoutId = null;
      }, delay);
    } else if (now - lastExecTime > delay) {
      // 超过delay时间,再次执行
      func.apply(this, args);
      lastExecTime = now;
    }
  };
}

const throttledLazyLoad = throttle(lazyLoad, 200); // 每200ms执行一次

window.addEventListener('scroll', throttledLazyLoad);
  • throttle 函数: 这是一个通用的函数节流函数, 接受一个函数 func 和一个延迟时间 delay 作为参数。
  • throttledLazyLoad: 使用 throttle 函数包装 lazyLoad 函数, 得到一个节流后的函数。
  • 事件监听: 监听 scroll 事件, 使用 throttledLazyLoad 函数。

3. Intersection Observer API

这是现代浏览器提供的一种更高效的懒加载方案。 Intersection Observer API 可以监听元素是否进入可视区域, 并且可以设置交叉比例, 非常灵活。

代码示例:

<!DOCTYPE html>
<html>
<head>
<title>Intersection Observer 懒加载</title>
<style>
  img {
    display: block;
    margin-bottom: 20px;
  }
</style>
</head>
<body>

  <img data-src="image1.jpg" src="placeholder.gif" alt="Image 1">
  <img data-src="image2.jpg" src="placeholder.gif" alt="Image 2">
  <img data-src="image3.jpg" src="placeholder.gif" alt="Image 3">
  <!-- 更多图片... -->

  <script>
    const images = document.querySelectorAll('img[data-src]');

    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.removeAttribute('data-src');
          observer.unobserve(img); // 停止监听已加载的图片
        }
      });
    });

    images.forEach(img => {
      observer.observe(img); // 开始监听每个图片
    });
  </script>

</body>
</html>
  • IntersectionObserver: 创建一个 IntersectionObserver 实例, 它的构造函数接受一个回调函数, 当被监听的元素进入或离开可视区域时, 就会触发这个回调函数。
  • entries: 回调函数的第一个参数是一个 entries 数组, 包含了所有被监听的元素的信息。
  • isIntersecting: entry.isIntersecting 属性表示元素是否进入了可视区域。
  • observer.observe(img): 开始监听一个元素。
  • observer.unobserve(img): 停止监听一个元素。

Intersection Observer API 的优点是性能高, 并且可以设置交叉比例, 非常灵活。 缺点是兼容性不如传统方案, 需要做一些 polyfill。

4. 原生 loading="lazy" 属性

现代浏览器(Chrome 76+, Firefox 75+, Safari 14+)已经支持原生的懒加载属性 loading="lazy"。 使用起来非常简单, 只需要在 img 标签上添加这个属性即可。

代码示例:

<img src="image1.jpg" loading="lazy" alt="Image 1">
<img src="image2.jpg" loading="lazy" alt="Image 2">
<img src="image3.jpg" loading="lazy" alt="Image 3">
  • loading="lazy": 告诉浏览器对图片进行懒加载。

loading="lazy" 属性的优点是使用简单, 性能好。 缺点是兼容性不如传统方案, 需要做一些降级处理。

三、 性能优化策略

光实现了懒加载还不够, 我们还需要考虑一些性能优化策略, 让懒加载的效果更好。

1. 使用占位图

在图片加载完成之前, 使用一张占位图来填充 img 标签。 占位图可以是:

  • 纯色背景: 使用与图片背景色相同的颜色作为占位图。
  • 低质量图片: 使用一张非常小的低质量图片作为占位图。
  • SVG 图标: 使用一个 SVG 图标作为占位图。

使用占位图可以避免页面闪烁, 提升用户体验。

2. 预加载

对于一些重要的图片, 例如首屏图片, 我们可以进行预加载, 避免用户等待。

预加载的方法有很多, 例如:

  • <link rel="preload">: 使用 <link rel="preload"> 标签来预加载图片。
  • Image 对象: 使用 Image 对象来预加载图片。

代码示例:

<link rel="preload" href="image1.jpg" as="image">
const img = new Image();
img.src = 'image1.jpg';

3. 图片压缩

对图片进行压缩可以减少图片的大小, 加快加载速度。 可以使用一些在线图片压缩工具, 例如 TinyPNG。

4. 使用 CDN

使用 CDN 可以将图片缓存到离用户更近的服务器上, 加快加载速度。

5. 避免重绘和回流

在懒加载的过程中, 尽量避免重绘和回流。 例如, 不要频繁地修改 img 标签的样式。

四、 不同方案的对比

为了方便大家选择合适的懒加载方案, 我整理了一个表格, 对比了不同方案的优缺点:

方案 优点 缺点 适用场景
offsetTop + innerHeight + pageYOffset 兼容性好, 实现简单。 性能差, 每次滚动都会遍历所有图片。 对兼容性要求高的项目, 可以作为兜底方案。
函数节流 解决了传统方案的性能问题, 减少了 lazyLoad 函数的执行频率。 仍然需要遍历所有图片, 只是降低了频率。 对性能有一定要求的项目, 可以作为过渡方案。
Intersection Observer API 性能高, 可以设置交叉比例, 非常灵活, 可以精确控制图片的加载时机。 兼容性不如传统方案, 需要做一些 polyfill。 对性能要求高的项目, 并且可以接受一定兼容性问题的项目。
loading="lazy" 使用简单, 性能好, 不需要编写任何 JavaScript 代码。 兼容性不如传统方案, 需要做一些降级处理。 对性能要求高的项目, 并且可以接受一定兼容性问题的项目。

五、 总结

图片懒加载是前端性能优化中非常重要的一环, 它可以有效地提升页面加载速度, 节省带宽, 提升用户体验。 选择合适的懒加载方案, 并结合一些性能优化策略, 可以让你的网站飞起来!

好了, 今天的讲座就到这里。 希望大家都能掌握图片懒加载的技巧, 写出更高效、更流畅的网站。 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注