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

前言

在电商、社交等图片密集型应用中,一次性加载所有图片会导致首屏白屏时间(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 在获取 offsetTop 或 getBoundingClientRect 时会强制浏览器重新计算布局(回流),在长列表场景下可能导致掉帧。方案 3 不受此影响


三、总结

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

四、补充

  • 现代浏览器(Chrome 76+)已原生支持 <img loading="lazy">,如果是简单场景,一行 HTML 属性即可搞定。
  • 务必给图片设置固定的宽高比或底色占位。否则图片加载前高度为 0,加载瞬间高度撑开会引发剧烈的页面抖动。
相关推荐
鬼手点金4 小时前
nanoGPT/nanochat 能做出 DeepSeek/Qwen 这样的大模型吗?
服务器·前端·javascript·bug·openclaw
软件技术NINI5 小时前
美妆商城html+Css+js 4页
javascript·css·html
蛋蛋的就会顺顺的5 小时前
hot100 p0——栈,贪心算法,动态规划
java·前端·javascript·算法·leetcode·力扣
程序员Sunday5 小时前
TypeScript 的 any、unknown、never 有什么区别?
前端·面试·typescript
朱涛的自习室5 小时前
70 美金的 Beyond Compare,我让 Gemini 4 和 DeepSeek 造个开源免费版(Day 2)
android·前端·人工智能
小 黄 鸡6 小时前
HashMap 面试必会知识点详解(JDK 8)
java·面试·职场和发展
码艺-Alimjan6 小时前
Tauri 源码备份 · 打包 & 解压 总结,源码收集之路
前端·javascript·vue.js
小马不起床6 小时前
零基础玩转 FastAPI_11:前端后台管理页面
前端·状态模式·fastapi
子非鱼a7 小时前
【文件上传】一个普普通通的edu16
前端
美狐美颜SDK开放平台8 小时前
直播APP如何实现美白滤镜功能?视频美颜SDK技术开发方案详解
前端·数据库·人工智能·音视频·美颜sdk·直播美颜sdk·视频美颜sdk