图片懒加载原理还是要懂的,2个月前面试被问到

620 阅读4分钟

图片懒加载(Lazy Loading)是一种优化网页性能的技术,尤其在移动设备和网络条件较差的情况下效果显著。其核心原理是:只在图片进入浏览器视口(viewport)时才加载,而非页面加载时一次性加载所有图片

为什么需要图片懒加载?

  1. 减少初始加载时间:首屏加载时无需请求所有图片,减少HTTP请求数
  2. 节省带宽:未被用户看到的图片不会被加载
  3. 提高性能:减少浏览器内存占用,避免一次性解码大量图片

实现原理

图片懒加载的实现主要基于以下几点:

  1. 占位符技术:使用低分辨率图片、纯色块或骨架屏作为占位符
  2. 视口检测:判断图片是否进入可视区域
  3. 资源替换:当图片进入视口时,将占位符替换为真实图片

下面介绍几种常见的实现方式:

基础实现(滚动事件 + getBoundingClientRect)

这是最基本的实现方式,通过监听窗口滚动事件,结合getBoundingClientRect()方法判断图片是否在视口中:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片懒加载示例</title>
    <style>
        .lazy-image {
            display: block;
            margin: 20px auto;
            min-height: 200px;
            background-color: #f0f0f0;
            transition: opacity 0.3s ease-in-out;
            opacity: 0.8;
        }
        .lazy-image.loaded {
            opacity: 1;
        }
    </style>
</head>
<body>
    <div style="height: 2000px;">
        <!-- 图片列表 -->
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=1" alt="风景图片1">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=2" alt="风景图片2">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=3" alt="风景图片3">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=4" alt="风景图片4">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=5" alt="风景图片5">
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const lazyImages = document.querySelectorAll('.lazy-image');
            
            // 处理图片加载
            function handleLazyLoad() {
                lazyImages.forEach(img => {
                    if (isInViewport(img) && !img.classList.contains('loaded')) {
                        loadImage(img);
                    }
                });
            }
            
            // 判断元素是否在视口中
            function isInViewport(element) {
                const rect = element.getBoundingClientRect();
                return (
                    rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
                    rect.bottom >= 0
                );
            }
            
            // 加载图片
            function loadImage(img) {
                const src = img.getAttribute('data-src');
                const image = new Image();
                
                image.onload = function() {
                    img.src = src;
                    img.classList.add('loaded');
                };
                
                image.src = src;
            }
            
            // 初始检查
            handleLazyLoad();
            
            // 监听滚动事件
            window.addEventListener('scroll', handleLazyLoad);
            
            // 监听窗口调整大小事件
            window.addEventListener('resize', handleLazyLoad);
        });
    </script>
</body>
</html>

优化实现(Intersection Observer API)

现代浏览器提供了更高效的Intersection Observer API,可以异步观察目标元素与祖先元素或视口的交叉状态:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片懒加载示例(Intersection Observer)</title>
    <style>
        .lazy-image {
            display: block;
            margin: 20px auto;
            min-height: 200px;
            background-color: #f0f0f0;
            transition: opacity 0.3s ease-in-out;
            opacity: 0.8;
        }
        .lazy-image.loaded {
            opacity: 1;
        }
    </style>
</head>
<body>
    <div style="height: 2000px;">
        <!-- 图片列表 -->
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=1" alt="风景图片1">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=2" alt="风景图片2">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=3" alt="风景图片3">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=4" alt="风景图片4">
        <img class="lazy-image" data-src="https://picsum.photos/800/400?random=5" alt="风景图片5">
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const lazyImages = document.querySelectorAll('.lazy-image');
            
            // 检查浏览器是否支持IntersectionObserver
            if ('IntersectionObserver' in window) {
                const observer = new IntersectionObserver((entries, observer) => {
                    entries.forEach(entry => {
                        if (entry.isIntersecting) {
                            const img = entry.target;
                            const src = img.getAttribute('data-src');
                            
                            const image = new Image();
                            image.onload = function() {
                                img.src = src;
                                img.classList.add('loaded');
                            };
                            
                            image.src = src;
                            observer.unobserve(img);
                        }
                    });
                }, {
                    rootMargin: '100px 0px', // 提前100px加载
                    threshold: 0.01
                });
                
                lazyImages.forEach(img => {
                    observer.observe(img);
                });
            } else {
                // 回退到传统滚动监听方式
                function handleLazyLoad() {
                    lazyImages.forEach(img => {
                        if (isInViewport(img) && !img.classList.contains('loaded')) {
                            const src = img.getAttribute('data-src');
                            
                            const image = new Image();
                            image.onload = function() {
                                img.src = src;
                                img.classList.add('loaded');
                            };
                            
                            image.src = src;
                        }
                    });
                }
                
                function isInViewport(element) {
                    const rect = element.getBoundingClientRect();
                    return (
                        rect.top <= (window.innerHeight + 100) &&
                        rect.bottom >= -100
                    );
                }
                
                window.addEventListener('scroll', handleLazyLoad);
                window.addEventListener('resize', handleLazyLoad);
                handleLazyLoad(); // 初始检查
            }
        });
    </script>
</body>
</html>

HTML原生实现(loading="lazy")

HTML5新增了loading="lazy"属性,提供了最简单的懒加载方式:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>原生图片懒加载示例</title>
    <style>
        img {
            display: block;
            margin: 20px auto;
            min-height: 200px;
            background-color: #f0f0f0;
        }
    </style>
</head>
<body>
    <div style="height: 2000px;">
        <!-- 普通图片 - 立即加载 -->
        <img src="https://picsum.photos/800/400?random=0" alt="首屏图片">
        
        <!-- 懒加载图片 -->
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" 
             data-src="https://picsum.photos/800/400?random=1" 
             alt="风景图片1" 
             loading="lazy">
             
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" 
             data-src="https://picsum.photos/800/400?random=2" 
             alt="风景图片2" 
             loading="lazy">
             
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" 
             data-src="https://picsum.photos/800/400?random=3" 
             alt="风景图片3" 
             loading="lazy">
    </div>

    <script>
        // 为支持loading="lazy"的浏览器设置真实src
        if ('loading' in HTMLImageElement.prototype) {
            const images = document.querySelectorAll('img[loading="lazy"]');
            images.forEach(img => {
                img.src = img.dataset.src;
            });
        } else {
            // 回退到IntersectionObserver或滚动监听
            // 这里可以插入前面的IntersectionObserver实现代码
        }
    </script>
</body>
</html>

性能对比与最佳实践

方法兼容性性能实现难度
滚动事件中等
Intersection Observer中等
loading="lazy"最高

最佳实践建议

  1. 优先使用loading="lazy",配合polyfill处理不支持的浏览器
  2. 对于复杂场景或需要更多控制的情况,使用Intersection Observer
  3. 为懒加载图片设置适当的占位符,避免布局抖动
  4. 考虑图片在视口外提前加载的距离(rootMargin)
  5. 对于不支持现代API的浏览器提供回退方案

通过合理实现图片懒加载,可以显著提升网页性能和用户体验,尤其在移动设备和内容丰富的页面中效果更为明显。