Appearance
图片懒加载
第一部分:懒加载基础
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性能优化的重要手段,合理实施能显著提升页面加载速度和用户体验。