Skip to content

图片懒加载

第一部分:懒加载基础

1.1 什么是图片懒加载

图片懒加载(Lazy Loading)是一种延迟加载技术,只在图片即将进入视口时才开始加载,而不是页面加载时一次性加载所有图片,从而提升页面性能和用户体验。

核心概念:

javascript
// 传统加载:所有图片立即加载
<img src="image1.jpg" alt="Image 1" />
<img src="image2.jpg" alt="Image 2" />
<img src="image3.jpg" alt="Image 3" />
// 问题:页面加载慢,浪费带宽

// 懒加载:按需加载
<img data-src="image1.jpg" alt="Image 1" />
<img data-src="image2.jpg" alt="Image 2" />
<img data-src="image3.jpg" alt="Image 3" />
// 优势:快速首屏,节省带宽

1.2 懒加载的优势

javascript
// 1. 性能提升
// - 减少初始加载时间
// - 降低首屏渲染时间
// - 节省带宽
// - 减少服务器负载

// 2. 用户体验
// - 更快的页面响应
// - 平滑的滚动体验
// - 节省用户流量
// - 避免加载不必要的资源

// 3. SEO优化
// - 提升页面加载速度
// - 改善Core Web Vitals指标
// - 更好的搜索排名

1.3 实现原理

javascript
// 基本原理
// 1. 初始状态:img标签使用占位符或data-src存储真实URL
// 2. 检测可见性:使用Intersection Observer监听元素可见性
// 3. 触发加载:元素进入视口时,将data-src赋值给src
// 4. 完成加载:图片加载完成后显示

// 简单实现
function lazyLoadImage(img) {
  const src = img.getAttribute('data-src');
  if (!src) return;
  
  img.src = src;
  img.onload = () => {
    img.removeAttribute('data-src');
    img.classList.add('loaded');
  };
}

// 使用Intersection Observer
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      lazyLoadImage(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

1.4 基础实现

javascript
// React基础懒加载组件
function LazyImage({ src, alt, placeholder }) {
  const [imageSrc, setImageSrc] = useState(placeholder);
  const [imageRef, setImageRef] = useState();
  
  useEffect(() => {
    let observer;
    
    if (imageRef) {
      observer = new IntersectionObserver(
        ([entry]) => {
          if (entry.isIntersecting) {
            setImageSrc(src);
            observer.unobserve(imageRef);
          }
        },
        {
          threshold: 0.01,
          rootMargin: '50px'
        }
      );
      
      observer.observe(imageRef);
    }
    
    return () => {
      if (observer && imageRef) {
        observer.unobserve(imageRef);
      }
    };
  }, [imageRef, src]);
  
  return (
    <img
      ref={setImageRef}
      src={imageSrc}
      alt={alt}
      loading="lazy"
    />
  );
}

// 使用
function Gallery() {
  return (
    <div className="gallery">
      <LazyImage 
        src="image1.jpg" 
        alt="Image 1"
        placeholder="data:image/svg+xml,%3Csvg..."
      />
      <LazyImage 
        src="image2.jpg" 
        alt="Image 2"
        placeholder="placeholder.jpg"
      />
    </div>
  );
}

第二部分:实现方案

2.1 原生loading属性

javascript
// 最简单的方案:使用原生loading属性
function NativeLazy() {
  return (
    <div>
      <img 
        src="image1.jpg" 
        alt="Image 1" 
        loading="lazy"
      />
      <img 
        src="image2.jpg" 
        alt="Image 2" 
        loading="lazy"
      />
    </div>
  );
}

// 优点:
// - 无需JavaScript
// - 浏览器原生支持
// - 性能最优
// - 实现简单

// 缺点:
// - 浏览器兼容性(需要polyfill)
// - 功能有限
// - 无法自定义行为

// 兼容性检查
function LazyImageWithFallback({ src, alt }) {
  const supportsLazyLoading = 'loading' in HTMLImageElement.prototype;
  
  if (supportsLazyLoading) {
    return <img src={src} alt={alt} loading="lazy" />;
  }
  
  // 降级到自定义实现
  return <CustomLazyImage src={src} alt={alt} />;
}

2.2 Intersection Observer实现

javascript
// 完整的Intersection Observer实现
function useLazyLoad(options = {}) {
  const {
    threshold = 0.01,
    rootMargin = '50px',
    root = null
  } = options;
  
  const [ref, setRef] = useState(null);
  const [isVisible, setIsVisible] = useState(false);
  
  useEffect(() => {
    if (!ref) return;
    
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { threshold, rootMargin, root }
    );
    
    observer.observe(ref);
    
    return () => {
      if (ref) {
        observer.unobserve(ref);
      }
    };
  }, [ref, threshold, rootMargin, root]);
  
  return [setRef, isVisible];
}

// 使用自定义Hook
function LazyImage({ src, alt, placeholder = '' }) {
  const [ref, isVisible] = useLazyLoad({
    threshold: 0.1,
    rootMargin: '100px'
  });
  
  return (
    <img
      ref={ref}
      src={isVisible ? src : placeholder}
      alt={alt}
      className={isVisible ? 'loaded' : 'loading'}
    />
  );
}

// 高级实现:带加载状态
function AdvancedLazyImage({ src, alt, placeholder }) {
  const [ref, isVisible] = useLazyLoad();
  const [isLoaded, setIsLoaded] = useState(false);
  const [hasError, setHasError] = useState(false);
  
  const handleLoad = () => {
    setIsLoaded(true);
  };
  
  const handleError = () => {
    setHasError(true);
  };
  
  return (
    <div ref={ref} className="lazy-image-container">
      {!isVisible && (
        <img 
          src={placeholder} 
          alt={alt}
          className="placeholder"
        />
      )}
      
      {isVisible && !hasError && (
        <img
          src={src}
          alt={alt}
          onLoad={handleLoad}
          onError={handleError}
          className={isLoaded ? 'loaded' : 'loading'}
        />
      )}
      
      {hasError && (
        <div className="error-placeholder">
          Failed to load image
        </div>
      )}
    </div>
  );
}

2.3 React库实现

javascript
// 使用react-lazyload库
import LazyLoad from 'react-lazyload';

function LibraryLazyImage() {
  return (
    <LazyLoad 
      height={200} 
      offset={100}
      once
      placeholder={<ImagePlaceholder />}
    >
      <img src="image.jpg" alt="Lazy loaded" />
    </LazyLoad>
  );
}

// 使用react-lazy-load-image-component
import { LazyLoadImage } from 'react-lazy-load-image-component';
import 'react-lazy-load-image-component/src/effects/blur.css';

function BlurLazyImage() {
  return (
    <LazyLoadImage
      src="image.jpg"
      alt="Image"
      effect="blur"
      placeholderSrc="thumbnail.jpg"
    />
  );
}

// 使用react-intersection-observer
import { useInView } from 'react-intersection-observer';

function InViewLazyImage({ src, alt }) {
  const { ref, inView } = useInView({
    triggerOnce: true,
    threshold: 0.1,
    rootMargin: '50px'
  });
  
  return (
    <div ref={ref}>
      {inView && <img src={src} alt={alt} />}
    </div>
  );
}

2.4 渐进式图片加载

javascript
// 低质量图片占位符(LQIP)
function LQIPImage({ src, lqip, alt }) {
  const [imageSrc, setImageSrc] = useState(lqip);
  const [isLoaded, setIsLoaded] = useState(false);
  
  useEffect(() => {
    const img = new Image();
    img.src = src;
    img.onload = () => {
      setImageSrc(src);
      setIsLoaded(true);
    };
  }, [src]);
  
  return (
    <img
      src={imageSrc}
      alt={alt}
      className={isLoaded ? 'loaded' : 'loading'}
      style={{
        filter: isLoaded ? 'none' : 'blur(10px)',
        transition: 'filter 0.3s'
      }}
    />
  );
}

// Base64编码的微小占位符
const tinyPlaceholder = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiNlZWUiLz48L3N2Zz4=';

function TinyPlaceholderImage({ src, alt }) {
  const [ref, isVisible] = useLazyLoad();
  
  return (
    <img
      ref={ref}
      src={isVisible ? src : tinyPlaceholder}
      alt={alt}
    />
  );
}

// 渐进式JPEG
function ProgressiveImage({ src, alt }) {
  const [currentSrc, setCurrentSrc] = useState(null);
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (!isVisible) return;
    
    // 先加载低质量版本
    const lowQualityImg = new Image();
    lowQualityImg.src = src.replace('.jpg', '-low.jpg');
    lowQualityImg.onload = () => {
      setCurrentSrc(lowQualityImg.src);
      
      // 再加载高质量版本
      const highQualityImg = new Image();
      highQualityImg.src = src;
      highQualityImg.onload = () => {
        setCurrentSrc(highQualityImg.src);
      };
    };
  }, [isVisible, src]);
  
  return (
    <img
      ref={ref}
      src={currentSrc || tinyPlaceholder}
      alt={alt}
    />
  );
}

第三部分:高级技巧

3.1 响应式图片懒加载

javascript
// srcset + 懒加载
function ResponsiveLazyImage({ src, srcSet, sizes, alt }) {
  const [ref, isVisible] = useLazyLoad();
  const [currentSrcSet, setCurrentSrcSet] = useState('');
  
  useEffect(() => {
    if (isVisible) {
      setCurrentSrcSet(srcSet);
    }
  }, [isVisible, srcSet]);
  
  return (
    <img
      ref={ref}
      src={isVisible ? src : ''}
      srcSet={currentSrcSet}
      sizes={sizes}
      alt={alt}
    />
  );
}

// 使用
function Gallery() {
  return (
    <ResponsiveLazyImage
      src="image-800.jpg"
      srcSet="
        image-400.jpg 400w,
        image-800.jpg 800w,
        image-1200.jpg 1200w
      "
      sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
      alt="Responsive image"
    />
  );
}

// WebP + 降级
function WebPLazyImage({ src, alt }) {
  const [ref, isVisible] = useLazyLoad();
  const [imageSrc, setImageSrc] = useState('');
  
  useEffect(() => {
    if (!isVisible) return;
    
    const supportsWebP = document.createElement('canvas')
      .toDataURL('image/webp')
      .indexOf('data:image/webp') === 0;
    
    if (supportsWebP) {
      setImageSrc(src.replace(/\.(jpg|png)$/, '.webp'));
    } else {
      setImageSrc(src);
    }
  }, [isVisible, src]);
  
  return (
    <img
      ref={ref}
      src={imageSrc}
      alt={alt}
    />
  );
}

// picture元素懒加载
function PictureLazyLoad({ sources, fallback, alt }) {
  const [ref, isVisible] = useLazyLoad();
  
  return (
    <picture ref={ref}>
      {isVisible && sources.map((source, i) => (
        <source
          key={i}
          srcSet={source.srcSet}
          type={source.type}
          media={source.media}
        />
      ))}
      <img src={isVisible ? fallback : ''} alt={alt} />
    </picture>
  );
}

3.2 背景图片懒加载

javascript
// 背景图片懒加载
function LazyBackgroundImage({ imageUrl, children }) {
  const [ref, isVisible] = useLazyLoad();
  const [backgroundImage, setBackgroundImage] = useState('none');
  
  useEffect(() => {
    if (isVisible) {
      setBackgroundImage(`url(${imageUrl})`);
    }
  }, [isVisible, imageUrl]);
  
  return (
    <div
      ref={ref}
      style={{
        backgroundImage,
        backgroundSize: 'cover',
        backgroundPosition: 'center'
      }}
    >
      {children}
    </div>
  );
}

// CSS背景图片懒加载
function CSSBackgroundLazy({ className, imageUrl, children }) {
  const [ref, isVisible] = useLazyLoad();
  
  return (
    <div
      ref={ref}
      className={`${className} ${isVisible ? 'bg-loaded' : ''}`}
      style={isVisible ? { backgroundImage: `url(${imageUrl})` } : {}}
    >
      {children}
    </div>
  );
}

// 多个背景图片
function MultipleBackgrounds({ backgrounds }) {
  const [ref, isVisible] = useLazyLoad();
  const [loadedBgs, setLoadedBgs] = useState([]);
  
  useEffect(() => {
    if (!isVisible) return;
    
    Promise.all(
      backgrounds.map(bg => {
        return new Promise((resolve) => {
          const img = new Image();
          img.onload = () => resolve(bg);
          img.src = bg.url;
        });
      })
    ).then(setLoadedBgs);
  }, [isVisible, backgrounds]);
  
  const bgString = loadedBgs
    .map(bg => `url(${bg.url})`)
    .join(', ');
  
  return (
    <div
      ref={ref}
      style={{
        backgroundImage: bgString
      }}
    />
  );
}

3.3 虚拟列表懒加载

javascript
// 结合虚拟列表
import { FixedSizeList } from 'react-window';

function VirtualListWithLazy({ items }) {
  const Row = ({ index, style }) => {
    const item = items[index];
    
    return (
      <div style={style}>
        <LazyImage
          src={item.imageUrl}
          alt={item.title}
          placeholder={item.thumbnail}
        />
        <h3>{item.title}</h3>
      </div>
    );
  };
  
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={120}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

// 无限滚动 + 懒加载
function InfiniteScrollLazy() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  
  const loadMore = useCallback(async () => {
    const newItems = await fetchItems(page);
    
    if (newItems.length === 0) {
      setHasMore(false);
    } else {
      setItems(prev => [...prev, ...newItems]);
      setPage(p => p + 1);
    }
  }, [page]);
  
  const [sentinelRef] = useInView({
    onChange: (inView) => {
      if (inView && hasMore) {
        loadMore();
      }
    }
  });
  
  return (
    <div>
      {items.map(item => (
        <LazyImage
          key={item.id}
          src={item.imageUrl}
          alt={item.title}
        />
      ))}
      {hasMore && <div ref={sentinelRef}>Loading...</div>}
    </div>
  );
}

3.4 预加载策略

javascript
// 预加载下一张图片
function PreloadNextImage({ images, currentIndex }) {
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (isVisible && currentIndex < images.length - 1) {
      const nextImage = new Image();
      nextImage.src = images[currentIndex + 1];
    }
  }, [isVisible, currentIndex, images]);
  
  return (
    <img
      ref={ref}
      src={images[currentIndex]}
      alt={`Image ${currentIndex}`}
    />
  );
}

// 智能预加载
function SmartPreload({ images, currentIndex }) {
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (!isVisible) return;
    
    // 预加载前后各2张
    const preloadRange = [-2, -1, 1, 2];
    
    preloadRange.forEach(offset => {
      const index = currentIndex + offset;
      if (index >= 0 && index < images.length) {
        const img = new Image();
        img.src = images[index];
      }
    });
  }, [isVisible, currentIndex, images]);
  
  return (
    <img
      ref={ref}
      src={images[currentIndex]}
      alt={`Image ${currentIndex}`}
    />
  );
}

// 基于连接速度的预加载
function AdaptivePreload({ images }) {
  const [preloadCount, setPreloadCount] = useState(1);
  
  useEffect(() => {
    const connection = navigator.connection;
    
    if (connection) {
      const effectiveType = connection.effectiveType;
      
      if (effectiveType === '4g') {
        setPreloadCount(3);
      } else if (effectiveType === '3g') {
        setPreloadCount(1);
      } else {
        setPreloadCount(0);
      }
    }
  }, []);
  
  useEffect(() => {
    images.slice(0, preloadCount).forEach(src => {
      const img = new Image();
      img.src = src;
    });
  }, [images, preloadCount]);
  
  return (
    <div>
      {images.map((src, i) => (
        <LazyImage key={i} src={src} alt={`Image ${i}`} />
      ))}
    </div>
  );
}

第四部分:性能优化

4.1 占位符优化

javascript
// SVG占位符
const svgPlaceholder = (width, height, color = '#eee') => `
  data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}'%3E
    %3Crect width='${width}' height='${height}' fill='${color}'/%3E
  %3C/svg%3E
`;

function SVGPlaceholderImage({ src, alt, width, height }) {
  return (
    <LazyImage
      src={src}
      alt={alt}
      placeholder={svgPlaceholder(width, height)}
    />
  );
}

// 模糊占位符
function BlurHashImage({ src, alt, blurhash }) {
  const [ref, isVisible] = useLazyLoad();
  const canvasRef = useRef();
  
  useEffect(() => {
    if (!isVisible && canvasRef.current && blurhash) {
      const pixels = decode(blurhash, 32, 32);
      const ctx = canvasRef.current.getContext('2d');
      const imageData = ctx.createImageData(32, 32);
      imageData.data.set(pixels);
      ctx.putImageData(imageData, 0, 0);
    }
  }, [isVisible, blurhash]);
  
  return (
    <div ref={ref} className="blur-container">
      {!isVisible && (
        <canvas
          ref={canvasRef}
          width={32}
          height={32}
          style={{ width: '100%', height: '100%', filter: 'blur(10px)' }}
        />
      )}
      {isVisible && <img src={src} alt={alt} />}
    </div>
  );
}

// 骨架屏占位符
function SkeletonImage({ src, alt, aspectRatio = '16/9' }) {
  const [ref, isVisible] = useLazyLoad();
  const [isLoaded, setIsLoaded] = useState(false);
  
  return (
    <div
      ref={ref}
      className="skeleton-container"
      style={{ aspectRatio }}
    >
      {!isLoaded && <div className="skeleton-loader" />}
      {isVisible && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setIsLoaded(true)}
          style={{ opacity: isLoaded ? 1 : 0 }}
        />
      )}
    </div>
  );
}

4.2 批量加载优化

javascript
// 批量懒加载管理器
class LazyLoadManager {
  constructor() {
    this.queue = [];
    this.loading = new Set();
    this.maxConcurrent = 3;
  }
  
  add(url, callback) {
    this.queue.push({ url, callback });
    this.process();
  }
  
  async process() {
    if (this.loading.size >= this.maxConcurrent) return;
    if (this.queue.length === 0) return;
    
    const { url, callback } = this.queue.shift();
    this.loading.add(url);
    
    try {
      const img = new Image();
      await new Promise((resolve, reject) => {
        img.onload = resolve;
        img.onerror = reject;
        img.src = url;
      });
      
      callback(url);
    } finally {
      this.loading.delete(url);
      this.process();
    }
  }
}

const lazyLoadManager = new LazyLoadManager();

function ManagedLazyImage({ src, alt }) {
  const [imageSrc, setImageSrc] = useState('');
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (isVisible) {
      lazyLoadManager.add(src, setImageSrc);
    }
  }, [isVisible, src]);
  
  return <img ref={ref} src={imageSrc} alt={alt} />;
}

// 优先级加载
function PriorityLazyImage({ src, alt, priority = 'normal' }) {
  const [imageSrc, setImageSrc] = useState('');
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (!isVisible) return;
    
    const img = new Image();
    img.src = src;
    
    if (priority === 'high') {
      img.fetchPriority = 'high';
    } else if (priority === 'low') {
      img.fetchPriority = 'low';
    }
    
    img.onload = () => setImageSrc(src);
  }, [isVisible, src, priority]);
  
  return <img ref={ref} src={imageSrc} alt={alt} />;
}

4.3 内存优化

javascript
// 图片缓存清理
function useLazyImageCache(maxSize = 50) {
  const cacheRef = useRef(new Map());
  
  const addToCache = useCallback((url) => {
    const cache = cacheRef.current;
    
    if (cache.size >= maxSize) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }
    
    cache.set(url, Date.now());
  }, [maxSize]);
  
  const isInCache = useCallback((url) => {
    return cacheRef.current.has(url);
  }, []);
  
  return { addToCache, isInCache };
}

// 使用缓存
function CachedLazyImage({ src, alt }) {
  const [imageSrc, setImageSrc] = useState('');
  const [ref, isVisible] = useLazyLoad();
  const { addToCache, isInCache } = useLazyImageCache();
  
  useEffect(() => {
    if (!isVisible) return;
    
    if (isInCache(src)) {
      setImageSrc(src);
    } else {
      const img = new Image();
      img.onload = () => {
        setImageSrc(src);
        addToCache(src);
      };
      img.src = src;
    }
  }, [isVisible, src, isInCache, addToCache]);
  
  return <img ref={ref} src={imageSrc} alt={alt} />;
}

// 离屏图片卸载
function UnloadOffscreenImages({ images }) {
  const containerRef = useRef();
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          const img = entry.target;
          
          if (!entry.isIntersecting) {
            // 离开视口,卸载图片
            img.src = '';
          } else {
            // 进入视口,重新加载
            img.src = img.dataset.src;
          }
        });
      },
      { rootMargin: '200px' }
    );
    
    const imgs = containerRef.current.querySelectorAll('img');
    imgs.forEach(img => observer.observe(img));
    
    return () => observer.disconnect();
  }, []);
  
  return (
    <div ref={containerRef}>
      {images.map(img => (
        <img key={img.id} data-src={img.url} alt={img.alt} />
      ))}
    </div>
  );
}

4.4 错误处理

javascript
// 完善的错误处理
function RobustLazyImage({ src, alt, fallback }) {
  const [imageSrc, setImageSrc] = useState('');
  const [error, setError] = useState(null);
  const [retryCount, setRetryCount] = useState(0);
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (!isVisible) return;
    
    const img = new Image();
    
    img.onload = () => {
      setImageSrc(src);
      setError(null);
    };
    
    img.onerror = () => {
      if (retryCount < 3) {
        setTimeout(() => {
          setRetryCount(c => c + 1);
        }, 1000 * (retryCount + 1));
      } else {
        setError('Failed to load image');
        if (fallback) {
          setImageSrc(fallback);
        }
      }
    };
    
    img.src = src;
  }, [isVisible, src, retryCount, fallback]);
  
  return (
    <div ref={ref}>
      {error && !fallback && (
        <div className="error-message">{error}</div>
      )}
      {imageSrc && <img src={imageSrc} alt={alt} />}
    </div>
  );
}

// 降级策略
function FallbackLazyImage({ src, fallbacks = [], alt }) {
  const [currentSrc, setCurrentSrc] = useState('');
  const [attemptIndex, setAttemptIndex] = useState(0);
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (!isVisible) return;
    
    const sources = [src, ...fallbacks];
    const currentSource = sources[attemptIndex];
    
    if (!currentSource) return;
    
    const img = new Image();
    
    img.onload = () => {
      setCurrentSrc(currentSource);
    };
    
    img.onerror = () => {
      if (attemptIndex < sources.length - 1) {
        setAttemptIndex(i => i + 1);
      }
    };
    
    img.src = currentSource;
  }, [isVisible, src, fallbacks, attemptIndex]);
  
  return <img ref={ref} src={currentSrc} alt={alt} />;
}

注意事项

1. SEO考虑

javascript
// 确保搜索引擎能抓取图片
function SEOFriendlyLazy({ src, alt }) {
  return (
    <img
      src={src}
      alt={alt}
      loading="lazy"
      // 提供完整的src而不是data-src
      // 搜索引擎能正确索引
    />
  );
}

// 提供结构化数据
function StructuredDataImage({ src, alt, schema }) {
  return (
    <>
      <script type="application/ld+json">
        {JSON.stringify(schema)}
      </script>
      <img src={src} alt={alt} loading="lazy" />
    </>
  );
}

2. 可访问性

javascript
// 保证可访问性
function AccessibleLazy({ src, alt, ariaLabel }) {
  const [ref, isVisible] = useLazyLoad();
  
  return (
    <img
      ref={ref}
      src={isVisible ? src : ''}
      alt={alt}
      aria-label={ariaLabel}
      role="img"
    />
  );
}

3. 性能监控

javascript
// 监控懒加载性能
function MonitoredLazy({ src, alt }) {
  const [ref, isVisible] = useLazyLoad();
  
  useEffect(() => {
    if (isVisible) {
      const startTime = performance.now();
      
      const img = new Image();
      img.onload = () => {
        const loadTime = performance.now() - startTime;
        
        // 上报性能数据
        analytics.track('image_load', {
          url: src,
          loadTime,
          visible: isVisible
        });
      };
      img.src = src;
    }
  }, [isVisible, src]);
  
  return <img ref={ref} src={isVisible ? src : ''} alt={alt} />;
}

常见问题

Q1: loading="lazy"和Intersection Observer哪个好?

A: loading="lazy"更简单,但Intersection Observer更灵活可控。

Q2: 懒加载会影响SEO吗?

A: 使用原生loading="lazy"不影响,自定义实现需注意提供完整src。

Q3: 如何处理图片加载失败?

A: 提供fallback图片或显示错误提示,实现重试机制。

Q4: 懒加载适合所有图片吗?

A: 首屏重要图片不建议懒加载,折叠下方内容适合。

Q5: 如何优化占位符?

A: 使用轻量SVG、BlurHash或骨架屏。

Q6: 懒加载影响用户体验吗?

A: 合理使用能提升体验,配合占位符避免布局偏移。

Q7: 如何测试懒加载?

A: 使用Chrome DevTools的Network面板和Lighthouse。

Q8: 移动端需要特殊处理吗?

A: 考虑网络状况,调整预加载策略和占位符。

Q9: 懒加载和预加载冲突吗?

A: 不冲突,可以智能预加载即将可见的图片。

Q10: React 19对懒加载有改进吗?

A: Suspense for Data Fetching可以更优雅地处理图片加载。

总结

核心要点

1. 懒加载优势
   ✅ 提升首屏速度
   ✅ 节省带宽
   ✅ 改善性能指标
   ✅ 提升用户体验

2. 实现方案
   ✅ 原生loading属性
   ✅ Intersection Observer
   ✅ 第三方库
   ✅ 自定义实现

3. 优化策略
   ✅ 占位符优化
   ✅ 渐进式加载
   ✅ 预加载策略
   ✅ 错误处理

最佳实践

1. 选择方案
   ✅ 优先原生loading
   ✅ 复杂需求用Intersection Observer
   ✅ 考虑浏览器兼容性
   ✅ 提供降级方案

2. 性能优化
   ✅ 合理的加载阈值
   ✅ 轻量占位符
   ✅ 批量加载控制
   ✅ 内存管理

3. 用户体验
   ✅ 避免布局偏移
   ✅ 平滑过渡动画
   ✅ 加载状态提示
   ✅ 错误友好处理

图片懒加载是web性能优化的重要手段,合理实施能显著提升页面加载速度和用户体验。