欢迎光临
我们一直在努力

什么是懒加载?如何实现图片懒加载?

在这里插入图片描述

文章目录

    • **一、什么是懒加载?**
      • **核心思想**
      • **应用场景**
      • **懒加载 vs 预加载**
    • **二、为什么需要图片懒加载?**
      • **性能收益**
      • **SEO 友好**
    • **三、图片懒加载实现方案**
      • **方案1:原生 Intersection Observer API(推荐)**
      • **方案2:兼容性更好的滚动监听**
      • **方案3:响应式图片懒加载**
      • **方案4:背景图片懒加载**
    • **四、现代最佳实践**
      • **1. 使用原生 loading="lazy"**
      • **2. 结合 IntersectionObserver 和原生支持**
      • **3. 图片优化组合拳**
      • **4. 性能监控**
    • **五、常见问题与解决方案**
      • **问题1:布局抖动(Layout Shift)**
      • **问题2:SEO 优化**
      • **问题3:打印支持**
    • **六、框架集成**
      • **React 实现**
      • **Vue 实现**
    • **七、性能对比数据**
    • **总结**
      • **选择策略**
      • **推荐库**
      • **黄金法则**

一、什么是懒加载?

懒加载是一种延迟加载资源的优化技术,只在资源需要显示或使用时才进行加载,而不是在页面初始化时一次性加载所有资源。

核心思想

  • 按需加载:可视区域内资源优先加载
  • 延迟加载:非关键资源延后加载
  • 异步加载:不阻塞页面渲染

应用场景

  • 图片懒加载:电商商品列表、新闻图集
  • 组件懒加载:SPA 路由组件
  • 脚本懒加载:第三方库、分析工具
  • iframe 懒加载:嵌入内容、广告
  • 懒加载 vs 预加载

    特性懒加载预加载
    时机 需要时加载 提前加载
    目标 非关键资源 关键资源
    网络 节省流量 占用带宽
    体验 可能延迟显示 更快呈现

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

    性能收益

    // 假设一个页面有 100 张图片
    // 无懒加载:同时发起 100 个请求,下载 10MB+
    // 有懒加载:首屏只加载 5-10 张,后续按需加载

    // 性能提升指标:
    // – 页面加载时间减少 30-50%
    // – 带宽节省 40-70%
    // – 内存占用降低
    // – 电池续航提升(移动端)

    SEO 友好

    • 现代搜索引擎(Google)支持懒加载
    • 不影响爬虫抓取内容
    • 提升页面速度得分(Core Web Vitals)

    三、图片懒加载实现方案

    方案1:原生 Intersection Observer API(推荐)

    <!– HTML 结构 –>
    <img
    class="lazy"
    data-src="real-image.jpg"
    src="placeholder.jpg"
    alt="描述"
    loading="lazy" <!– 浏览器原生懒加载 >

    width="800" <!– 避免布局偏移 –>
    height="600"
    >

    // 基础实现
    document.addEventListener("DOMContentLoaded", function() {
    const lazyImages = document.querySelectorAll('img.lazy');

    // 创建观察器
    const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    const img = entry.target;

    // 替换 src
    img.src = img.dataset.src;
    img.classList.remove('lazy');

    // 图片加载完成后的回调
    img.onload = () => {
    img.classList.add('loaded');
    };

    // 停止观察
    observer.unobserve(img);
    }
    });
    }, {
    // 配置选项
    root: null, // 视口
    rootMargin: '50px', // 提前 50px 加载
    threshold: 0.1 // 10% 可见时触发
    });

    // 观察所有懒加载图片
    lazyImages.forEach(img => imageObserver.observe(img));
    });

    优化版本:

    class LazyLoader {
    constructor(options = {}) {
    this.options = {
    root: null,
    rootMargin: '50px',
    threshold: 0.01,
    placeholder: 'data:image/svg+xml;base64,…', // 1×1 透明图
    errorImage: 'error.jpg',
    options
    };

    this.observer = null;
    this.init();
    }

    init() {
    // 检测浏览器支持
    if ('IntersectionObserver' in window) {
    this.initObserver();
    } else {
    this.fallbackPolyfill();
    }

    // 错误处理
    this.addErrorHandlers();
    }

    initObserver() {
    this.observer = new IntersectionObserver(this.handleIntersect.bind(this), this.options);

    document.querySelectorAll('[data-src], [data-srcset]').forEach(el => {
    // 设置占位图
    if (el.tagName === 'IMG' && !el.src) {
    el.src = this.options.placeholder;
    }
    this.observer.observe(el);
    });
    }

    handleIntersect(entries) {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    this.loadImage(entry.target);
    this.observer.unobserve(entry.target);
    }
    });
    }

    loadImage(element) {
    // 处理 src 和 srcset
    if (element.dataset.src) {
    element.src = element.dataset.src;
    delete element.dataset.src;
    }

    if (element.dataset.srcset) {
    element.srcset = element.dataset.srcset;
    delete element.dataset.srcset;
    }

    // 处理背景图
    if (element.dataset.bg) {
    element.style.backgroundImage = `url(${element.dataset.bg})`;
    delete element.dataset.bg;
    }
    }

    addErrorHandlers() {
    document.addEventListener('error', e => {
    if (e.target.tagName === 'IMG' && e.target.classList.contains('lazy')) {
    e.target.src = this.options.errorImage;
    e.target.onerror = null; // 防止循环
    }
    }, true);
    }

    fallbackPolyfill() {
    // 兼容方案:滚动事件 + 节流
    const lazyLoad = () => {
    const scrollTop = window.pageYOffset;
    document.querySelectorAll('[data-src]').forEach(img => {
    if (img.offsetTop < window.innerHeight + scrollTop) {
    this.loadImage(img);
    }
    });
    };

    window.addEventListener('scroll', throttle(lazyLoad, 200));
    window.addEventListener('resize', lazyLoad);
    window.addEventListener('orientationchange', lazyLoad);
    }
    }

    // 节流函数
    function throttle(fn, delay) {
    let timer = null;
    return function() {
    if (!timer) {
    timer = setTimeout(() => {
    fn.apply(this, arguments);
    timer = null;
    }, delay);
    }
    };
    }

    // 使用
    const lazyLoader = new LazyLoader({
    rootMargin: '100px',
    threshold: 0
    });

    方案2:兼容性更好的滚动监听

    // 适用于不支持 IntersectionObserver 的浏览器
    function lazyLoadImages() {
    const images = document.querySelectorAll('img[data-src]');
    const scrollTop = window.pageYOffset;
    const windowHeight = window.innerHeight;

    images.forEach(img => {
    const imgTop = img.getBoundingClientRect().top + scrollTop;

    // 判断是否进入可视区域
    if (imgTop < scrollTop + windowHeight + 200) { // 提前 200px 加载
    // 加载图片
    img.src = img.dataset.src;

    // 处理响应式图片
    if (img.dataset.srcset) {
    img.srcset = img.dataset.srcset;
    }

    // 移除 data 属性
    img.removeAttribute('data-src');

    // 图片加载完成
    img.onload = () => {
    img.classList.add('loaded');
    };

    // 加载失败处理
    img.onerror = () => {
    img.src = 'fallback.jpg';
    img.classList.add('error');
    };
    }
    });
    }

    // 使用节流优化
    const throttleLazyLoad = throttle(lazyLoadImages, 100);

    // 监听事件
    window.addEventListener('scroll', throttleLazyLoad);
    window.addEventListener('resize', throttleLazyLoad);
    window.addEventListener('orientationchange', throttleLazyLoad);

    // 初始加载
    document.addEventListener('DOMContentLoaded', lazyLoadImages);

    方案3:响应式图片懒加载

    <!– 支持 srcset 和 sizes –>
    <img
    class="lazy"
    data-src="image-800w.jpg"
    data-srcset="
    image-400w.jpg 400w,
    image-800w.jpg 800w,
    image-1200w.jpg 1200w
    "

    data-sizes="
    (max-width: 480px) 100vw,
    (max-width: 768px) 50vw,
    33vw
    "

    src="placeholder.jpg"
    alt="响应式图片"
    >

    // 处理响应式图片
    function loadResponsiveImage(img) {
    if (img.dataset.src) {
    img.src = img.dataset.src;
    }

    if (img.dataset.srcset) {
    img.srcset = img.dataset.srcset;
    }

    if (img.dataset.sizes) {
    img.sizes = img.dataset.sizes;
    }

    // 清理 data 属性
    ['src', 'srcset', 'sizes'].forEach(attr => {
    delete img.dataset[attr];
    });
    }

    方案4:背景图片懒加载

    <div
    class="lazy-bg"
    data-bg="image.jpg"
    style="background-image: url('placeholder.jpg');"
    >
    </div>

    // 背景图片懒加载
    function lazyLoadBackgrounds() {
    const elements = document.querySelectorAll('[data-bg]');

    const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    const element = entry.target;

    // 创建图片预加载
    const img = new Image();
    img.src = element.dataset.bg;

    img.onload = () => {
    // 应用背景图
    element.style.backgroundImage = `url(${element.dataset.bg})`;
    element.classList.add('loaded');
    delete element.dataset.bg;
    };

    observer.unobserve(element);
    }
    });
    });

    elements.forEach(el => observer.observe(el));
    }


    四、现代最佳实践

    1. 使用原生 loading=“lazy”

    <!– 最简单的方式,现代浏览器支持 –>
    <img
    src="image.jpg"
    loading="lazy"
    alt="描述"
    width="800"
    height="600"
    >

    <!– iframe 也支持 –>
    <iframe
    src="video.html"
    loading="lazy"
    width="800"
    height="600"
    >
    </iframe>

    浏览器支持检测:

    if ('loading' in HTMLImageElement.prototype) {
    // 浏览器支持原生懒加载
    console.log('Native lazy loading supported');
    } else {
    // 回退到 JavaScript 实现
    }

    2. 结合 IntersectionObserver 和原生支持

    // 智能懒加载选择器
    class SmartLazyLoad {
    constructor() {
    this.supportsNative = 'loading' in HTMLImageElement.prototype;
    this.init();
    }

    init() {
    if (this.supportsNative) {
    this.enableNativeLazyLoad();
    } else {
    this.enfallbackLazyLoad();
    }
    }

    enableNativeLazyLoad() {
    // 为所有图片添加 loading="lazy"(首屏图片除外)
    document.querySelectorAll('img:not([loading])').forEach((img, index) => {
    // 前 3 张图片立即加载(首屏)
    if (index >= 3) {
    img.loading = 'lazy';
    }
    });
    }

    // … 回退方案
    }

    3. 图片优化组合拳

    <!– 完整优化方案 –>
    <picture>
    <!– WebP 格式(优先) –>
    <source
    data-srcset="image.webp"
    type="image/webp"
    media="(min-width: 768px)"
    >

    <source
    data-srcset="image-mobile.webp"
    type="image/webp"
    media="(max-width: 767px)"
    >

    <!– 回退格式 –>
    <source
    data-srcset="image.jpg"
    type="image/jpeg"
    media="(min-width: 768px)"
    >

    <source
    data-srcset="image-mobile.jpg"
    type="image/jpeg"
    media="(max-width: 767px)"
    >

    <!– 最终回退 –>
    <img
    class="lazy"
    data-src="image.jpg"
    src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjYwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48L3N2Zz4="
    alt="优化图片"
    loading="lazy"
    width="800"
    height="600"
    onload="this.classList.add('loaded')"
    >

    </picture>

    4. 性能监控

    // 监控懒加载性能
    const lazyLoadMetrics = {
    images: [],
    startTime: performance.now()
    };

    // 记录图片加载时间
    document.querySelectorAll('img.lazy').forEach(img => {
    const start = performance.now();

    img.onload = () => {
    const loadTime = performance.now() start;
    lazyLoadMetrics.images.push({
    src: img.src,
    loadTime,
    visible: img.offsetTop < window.innerHeight
    });

    // 上报到监控系统
    if (lazyLoadMetrics.images.length === 1) {
    const firstImageLoad = performance.now() lazyLoadMetrics.startTime;
    reportMetric('firstLazyImageLoad', firstImageLoad);
    }
    };
    });


    五、常见问题与解决方案

    问题1:布局抖动(Layout Shift)

    解决:

    /* 1. 固定宽高 */
    .lazy-img {
    width: 100%;
    height: 0;
    padding-bottom: 75%; /* 4:3 比例 */
    background: #f5f5f5;
    overflow: hidden;
    }

    /* 2. 使用占位容器 */
    .img-wrapper {
    position: relative;
    width: 100%;
    height: 0;
    padding-bottom: 56.25%; /* 16:9 */
    }

    .img-wrapper img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    }

    /* 3. 渐入效果 */
    .lazy {
    opacity: 0;
    transition: opacity 0.3s;
    }

    .lazy.loaded {
    opacity: 1;
    }

    问题2:SEO 优化

    <!– 添加结构化数据 –>
    <script type="application/ld+json">
    {
    "@context": "https://schema.org",
    "@type": "ImageObject",
    "contentUrl": "real-image.jpg",
    "name": "图片标题",
    "description": "图片描述"
    }
    </script>

    <!– 确保爬虫能抓取 –>
    <noscript>
    <img src="real-image.jpg" alt="描述">
    </noscript>

    问题3:打印支持

    /* 打印时加载所有图片 */
    @media print {
    img.lazy {
    content: attr(data-src);
    }

    .lazy {
    background-image: attr(data-bg) !important;
    }
    }


    六、框架集成

    React 实现

    import React, { useEffect, useRef } from 'react';
    import { throttle } from 'lodash';

    const LazyImage = ({ src, alt, placeholder, …props }) => {
    const imgRef = useRef(null);

    useEffect(() => {
    const observer = new IntersectionObserver(
    (entries) => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    const img = entry.target;
    img.src = img.dataset.src;
    observer.unobserve(img);
    }
    });
    },
    { rootMargin: '50px' }
    );

    if (imgRef.current) {
    observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
    }, []);

    return (
    <img
    ref={imgRef}
    data-src={src}
    src={placeholder}
    alt={alt}
    {…props}
    />
    );
    };

    // 使用
    <LazyImage
    src="real-image.jpg"
    placeholder="data:image/svg+xml;base64,…"
    alt="描述"
    width={800}
    height={600}
    />

    Vue 实现

    <template>
    <img
    :src="placeholder"
    :data-src="realSrc"
    :alt="alt"
    ref="lazyImage"
    class="lazy-image"
    />
    </template>

    <script>
    export default {
    name: 'LazyImage',
    props: {
    realSrc: String,
    placeholder: {
    type: String,
    default: 'data:image/svg+xml;base64,…'
    },
    alt: String
    },
    mounted() {
    this.initLazyLoad();
    },
    methods: {
    initLazyLoad() {
    if ('IntersectionObserver' in window) {
    const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    this.loadImage();
    observer.unobserve(this.$refs.lazyImage);
    }
    });
    }, { threshold: 0.01 });

    observer.observe(this.$refs.lazyImage);
    } else {
    this.fallbackLoad();
    }
    },
    loadImage() {
    this.$refs.lazyImage.src = this.realSrc;
    this.$emit('loaded');
    },
    fallbackLoad() {
    window.addEventListener('scroll', this.checkVisibility);
    this.checkVisibility();
    },
    checkVisibility: throttle(function() {
    const rect = this.$refs.lazyImage.getBoundingClientRect();
    if (rect.top < window.innerHeight + 100) {
    this.loadImage();
    window.removeEventListener('scroll', this.checkVisibility);
    }
    }, 100)
    }
    };
    </script>


    七、性能对比数据

    方案首屏加载时间总带宽内存占用兼容性
    无懒加载 100% 100%
    滚动监听 30-40% 99%
    IntersectionObserver 很快 20-30% 95%
    原生 loading=“lazy” 最快 15-25% 最低 85%

    总结

    选择策略

  • 现代项目:优先使用 loading="lazy" + IntersectionObserver
  • 兼容性要求高:IntersectionObserver + 滚动监听回退
  • 框架项目:使用成熟的懒加载库
  • 推荐库

    • lozad.js:超轻量(1KB)
    • lazysizes:功能全面
    • react-lazyload:React 专用
    • vue-lazyload:Vue 专用

    黄金法则

  • 首屏图片不懒加载:保证关键内容立即显示
  • 设置固定宽高:避免布局偏移
  • 提供占位符:改善用户体验
  • 错误处理:加载失败要有降级方案
  • 性能监控:持续测量优化效果
  • 懒加载是现代 Web 优化的必备技术,合理使用可显著提升页面性能,特别是在移动端和弱网环境下效果更为明显。在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 什么是懒加载?如何实现图片懒加载?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址