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

前言

在电商、社交等图片密集型应用中,一次性加载所有图片会导致首屏白屏时间(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,加载瞬间高度撑开会引发剧烈的页面抖动。
相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
晨米酱3 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄3 天前
JavaScript 隐式全局变量解析
javascript
彧azz3 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端