图片懒加载:从原理到实战,让页面加载快到飞起!

1 阅读6分钟

还在为网页图片太多导致加载卡顿发愁?电商页面图片密密麻麻,首屏加载半天没反应?别慌!今天教你一招 “图片懒加载”,让页面加载速度提升 50%,初学者也能轻松上手~

一、为什么需要图片懒加载?先搞懂浏览器加载的 “痛点”

打开一个图片超多的网页(比如电商首页),你有没有发现:明明只看得到屏幕内的几张图,浏览器却在疯狂下载所有图片?这背后藏着性能 “大坑”!

(一)图片加载的 “秘密流程”

当你在<img>标签里写src="图片地址"时,浏览器会:

  1. 解析到src属性,立刻启动下载线程(浏览器通常有多个线程并发下载资源);
  2. 通过http/https协议(应用层协议),根据图片地址的 IP,向服务器发送请求;
  3. 经过 TCP/IP 协议传输数据,把图片字节码下载到本地,再渲染显示。

image.png

但问题来了:网络带宽是有限的!如果一上来就加载所有图片,会抢占 CSS、JS 等关键资源的下载通道,导致页面加载变慢。数据显示,页面加载延迟 0.5 秒,用户流失率会增加 20%!

(二)直接加载所有图片的 3 大问题

  • 首屏加载慢:用户打开页面,要等所有图片下载完才能看到内容,体验差;
  • 浪费流量:用户可能没滚动到页面底部,却下载了所有图片,尤其对移动端用户不友好;
  • 阻塞关键资源:图片下载占用线程,导致 CSS 渲染、JS 执行被拖延,页面 “卡壳”。

二、图片懒加载:只加载 “该加载” 的图片

(一)核心思路:“按需加载”

懒加载的本质很简单:只加载用户当前能看到的图片(可视区),滚动到哪里再加载哪里
比如你打开一个有 100 张图的页面,首屏只显示 5 张,就先下载这 5 张;当你往下滚动,再加载接下来进入视线的图片。

(二)实现关键:别用src存真实地址!

<img>标签的src属性是 “触发下载” 的开关 —— 只要src有值,浏览器就会立刻下载。所以懒加载的第一个技巧是:

  • 真实图片地址存在自定义属性里(比如data-original),不直接写在src里;
  • src暂时放一张占位图(比如小尺寸的 loading 动图),既占位又不占用太多带宽(占位图可以缓存,只下载一次)。

image.png

像这样:

<img 
  class="image-item" 
  lazyload="true"  <!-- 标记这是需要懒加载的图片 -->
  src="loading.gif"  <!-- 占位图 -->
  data-original="真实图片地址.jpg"  <!-- 真实地址存在自定义属性 -->
/>

三、手把手实现懒加载:从基础版到高级版

(一)基础版:监听滚动事件,判断图片是否在可视区

核心逻辑:监听页面滚动,每次滚动时检查图片是否进入可视区,进入则把data-original的值赋给src,触发下载。

代码实现:

<!-- HTML结构 -->
<img class="image-item" lazyload="true" src="loading.gif" data-original="pic1.jpg" />
<img class="image-item" lazyload="true" src="loading.gif" data-original="pic2.jpg" />
<!-- 更多图片... -->

<script>
  // 1. 获取所有需要懒加载的图片
  const imgs = document.querySelectorAll('img[data-original][lazyload]');
  //选中同时包含 data-original 和 lazyload 属性的 <img> 标签

  // 2. 定义懒加载函数:检查并加载可视区图片
  function lazyload() {
    // 可视区高度(当前屏幕能看到的高度)
    const viewHeight = document.documentElement.clientHeight;
    
    // 遍历所有图片
    imgs.forEach(img => {
      // 如果已经加载过(没有data-original了),跳过
      if (!img.dataset.original) return;

      // 获取图片位置:getBoundingClientRect()返回元素相对于可视区的坐标
      const rect = img.getBoundingClientRect();
      
      // 判断图片是否进入可视区:图片顶部 < 可视区高度,且图片底部 > 0
      if (rect.top < viewHeight && rect.bottom > 0) {
        // 创建新Image对象预加载,避免直接修改src导致的闪烁
        const newImg = new Image();
        newImg.src = img.dataset.original;
        
        // 图片加载完成后,替换src
        newImg.onload = () => {
          img.src = img.dataset.original; // 显示真实图片
          img.removeAttribute('data-original'); // 标记为已加载
          img.removeAttribute('lazyload');
        };
      }
    });
  }

  // 3. 触发时机:初始加载+滚动时
  window.addEventListener('scroll', lazyload); // 滚动时检查
  document.addEventListener('DOMContentLoaded', lazyload); // 页面初始渲染后检查
  window.addEventListener('resize', lazyload); // 窗口大小改变时检查
</script>

原理说明:

  • getBoundingClientRect():获取图片相对于可视区的位置,rect.top是图片顶部到可视区顶部的距离,rect.bottom是图片底部到可视区顶部的距离;
  • rect.top < viewHeightrect.bottom > 0时,说明图片有一部分在可视区内,需要加载。

image.png

(二)进阶版:解决滚动事件 “触发太频繁” 的问题

基础版有个缺陷:scroll事件会在滚动时疯狂触发(一秒可能触发几十次),每次都遍历所有图片 + 调用getBoundingClientRect()(会触发回流),可能导致性能问题。

解决办法:节流(throttle) —— 让函数在一段时间内只执行一次。

节流函数实现:

// 节流函数:每隔delay时间,只执行一次func
function throttle(func, delay = 100) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

// 用节流包装懒加载函数
const throttledLazyload = throttle(lazyload);
window.addEventListener('scroll', throttledLazyload); // 滚动时只按间隔执行

效果:

滚动时,lazyload函数不再频繁触发,而是每隔 100 毫秒执行一次,减少性能消耗。

(三)终极版:用IntersectionObserver彻底解放性能

现代浏览器提供了IntersectionObserverAPI,专门监听元素是否进入可视区,异步执行(不阻塞主线程),无需手动监听scroll事件,也不用节流!

代码实现:

<script>
  function initLazyload() {
    // 获取所有需要懒加载的图片
    const imgs = document.querySelectorAll('img[data-original][lazyload]');
    
    // 创建观察器
    const observer = new IntersectionObserver((changes) => {
      changes.forEach((change) => {
        // 当图片进入可视区(交叉比例>0)
        if (change.intersectionRatio > 0) {
          const img = change.target;
          // 加载图片
          const newImg = new Image();
          newImg.src = img.dataset.original;
          newImg.onload = () => {
            img.src = newImg.src;
            img.removeAttribute('data-original');
            img.removeAttribute('lazyload');
            observer.unobserve(img); // 加载完后停止观察,节省资源
          };
        }
      });
    });

    // 观察所有图片
    imgs.forEach(img => {
      observer.observe(img);
    });
  }

  // 页面加载后初始化
  document.addEventListener('DOMContentLoaded', initLazyload);
</script>

优势:

  • 无需监听scrollresize事件,代码更简洁;
  • 浏览器在后台异步计算元素是否可见,不阻塞 JS 执行;
  • 自动处理性能优化,无需手动节流。

四、懒加载实战技巧:这些细节让体验更丝滑

  1. 占位图设计:用和真实图片相同尺寸的占位图(比如 1x1 像素的透明图拉伸),避免图片加载时页面布局 “跳动”;

  2. 预加载一点点:让图片在进入可视区前提前加载(比如距离可视区底部 200px 时就开始加载),用IntersectionObserverrootMargin实现:

    // 提前200px加载
    const observer = new IntersectionObserver((changes) => { ... }, {
      rootMargin: '0px 0px 200px 0px' // 可视区底部向外扩展200px
    });
    
  3. 处理加载失败:给图片添加onerror事件,加载失败时显示默认图:

    newImg.onerror = () => {
      img.src = 'error.png'; // 加载失败显示错误图
    };
    
  4. 兼容旧浏览器IntersectionObserver在 IE 不支持,可降级使用基础版(配合节流)。

五、总结:懒加载让页面 “轻装上阵”

图片懒加载通过 “按需加载”,完美解决了多图页面的加载性能问题,核心步骤是:

  1. 真实地址存data-originalsrc放占位图;
  2. 监听图片是否进入可视区(基础版用scroll+ 节流,高级版用IntersectionObserver);
  3. 进入可视区后,将data-original赋值给src,加载图片。