前端性能优化:图片懒加载的三种手写方案

前言

在电商、社交等图片密集型应用中,一次性加载所有图片会导致首屏白屏时间(FP)过长,消耗用户大量流量。图片懒加载 的核心思想就是: "按需加载" ------只有当图片进入或即将进入可视区域时,才真正发起网络请求。

一、 核心原理

  1. 占位图 :初始化时,图片的 src 属性指向一张极小的 base64 图片或 loading 占位图。
  2. 存储地址 :将真实的图片 URL 存放在自定义属性中(如 data-src)。
  3. 触发判断 :通过 JS 监听位置变化,当图片进入可视区,将 data-src 的值赋给 src

二、 方案对比与实现

方案 1:传统滚动监听(Scroll + OffsetTop)

这是最基础的方案,通过计算绝对位置来判断。

公式: window.innerHeight (可视窗口高) + document.documentElement.scrollTop (滚动条高度) > element.offsetTop (元素距离页面顶部高度)

代码实现:

JavaScript 复制代码
function lazyLoad() {
  const images = document.querySelectorAll('img[data-src]');
  const clientHeight = window.innerHeight;
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

  images.forEach(img => {
    // 判断是否进入可视区
    if (clientHeight + scrollTop > img.offsetTop) {
      img.src = img.dataset.src;
      img.removeAttribute('data-src'); // 加载后移除属性,防止重复执行
    }
  });
}

// 注意:必须添加节流(Throttle),防止滚动时高频触发导致卡顿
window.addEventListener('scroll', throttle(lazyLoad, 200));

方案 2:现代位置属性(getBoundingClientRect)

此方法通过getBoundingClientRect获取元素相对于浏览器视口的位置来判断,逻辑更简洁。

判断条件: rect.top(元素顶部距离视口顶部的距离) < window.innerHeight

代码实现:

JavaScript 复制代码
function handleScroll() {
  const img = document.getElementById('target-img');
  const rect = img.getBoundingClientRect();
  const viewHeight = window.innerHeight || document.documentElement.clientHeight;

  // 元素顶部出现在视口内,且没有超出视口底部
  if (rect.top >= 0 && rect.top < viewHeight) {
    console.log('图片进入可视区,开始加载');
    img.src = img.dataset.src;
    window.removeEventListener('scroll', handleScroll); // 加载后卸载监听
  }
}

window.addEventListener('scroll', handleScroll);

方案 3:最优解方案(IntersectionObserver API)

IntersectionObserver这是目前最推荐的方案,它是异步的,不会阻塞主线程,且不需要手动计算位置,性能最高。

使用语法:const observer = new IntersectionObserver(callback, options)

  • callback:是元素可见性发生变化时的回调函数,接收两个参数:
    • entries:观察目标的对象数组。对象中存在isIntersecting属性(布尔值),代表目标元素是否与根元素交叉(即进入视口)。
  • options:配置对象(该参数可选)。其中 root表示指定一个根元素,默认是浏览器窗口、rootMargin表示控制根元素的外边距、threshold 为目标元素与根元素中的可见比例,可以通过设置值来触发回调函数

代码实现:

JavaScript 复制代码
const observerOptions = {
  root: null, // 默认为浏览器视口
  rootMargin: '0px 0px 50px 0px', // 提前 50px 触发加载,提升用户体验
  threshold: 0.1 // 交叉比例达到 10% 时触发
};

const handleIntersection = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      console.log('IntersectionObserver 推送:图片加载成功');
      observer.unobserve(img); // 停止观察该元素
    }
  });
};

const observer = new IntersectionObserver(handleIntersection, observerOptions);
// 观察页面中所有带 data-src 的图片
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));

方案 1 和 2 在获取 offsetTopgetBoundingClientRect 时会强制浏览器重新计算布局(回流),在长列表场景下可能导致掉帧。方案 3 不受此影响


三、总结

特性 方案 1 (OffsetTop) 方案 2 (Rect) 方案 3 (Intersection)
计算复杂度 较高 (需计算累计高度) 中等 极低 (引擎原生实现)
性能消耗 高 (频繁触发回流) 高 (触发回流) 低 (异步非阻塞)
兼容性 极好 (所有浏览器) 好 (IE9+) 一般 (现代浏览器, IE 需 Polyfill)

四、补充

  • 现代浏览器(Chrome 76+)已原生支持 <img loading="lazy">,如果是简单场景,一行 HTML 属性即可搞定。
  • 务必给图片设置固定的宽高比或底色占位。否则图片加载前高度为 0,加载瞬间高度撑开会引发剧烈的页面抖动。
相关推荐
不爱吃糖的程序媛9 小时前
Flutter 与 OpenHarmony 通信:Flutter Channel 使用指南
前端·javascript·flutter
利刃大大9 小时前
【Vue】Element-Plus快速入门 && Form && Card && Table && Tree && Dialog && Menu
前端·javascript·vue.js·element-plus
NEXT069 小时前
AI 应用工程化实战:使用 LangChain.js 编排 DeepSeek 复杂工作流
前端·javascript·langchain
念风零壹9 小时前
AI 时代的前端技术:从系统编程到 JavaScript/TypeScript
前端·ai
光影少年10 小时前
react的hooks防抖和节流是怎样做的
前端·javascript·react.js
小毛驴85010 小时前
Vue 路由示例
前端·javascript·vue.js
发现一只大呆瓜10 小时前
AI流式交互:SSE与WebSocket技术选型
前端·javascript·面试
园小异10 小时前
2026年技术面试完全指南:从算法到系统设计的实战突破
算法·面试·职场和发展
m0_7190841111 小时前
React笔记张天禹
前端·笔记·react.js