益阳市文章资讯

一文手把手教你如何使用JavaScript预加载图片告别加载卡顿

2026-03-24 15:44:01 浏览次数:3
详细信息
JavaScript图片预加载教程:告别页面卡顿

本文将手把手教你如何使用JavaScript实现图片预加载,提升用户体验,告别加载卡顿问题。

一、为什么需要图片预加载?

当网页包含大量图片时,用户浏览过程中可能会遇到:

图片预加载可以在后台提前加载图片,当需要显示时直接从缓存读取,提供无缝的用户体验。

二、基本原理

图片预加载的核心原理是:创建一个Image对象,设置其src属性,浏览器会自动下载图片并缓存。当实际需要显示该图片时,浏览器会从缓存中快速读取。

三、基础实现方法

1. 单张图片预加载

function preloadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();

    img.onload = () => {
      console.log(`图片加载成功: ${url}`);
      resolve(img);
    };

    img.onerror = () => {
      console.error(`图片加载失败: ${url}`);
      reject(new Error(`加载失败: ${url}`));
    };

    // 开始加载
    img.src = url;
  });
}

// 使用示例
preloadImage('https://example.com/image.jpg')
  .then(img => {
    console.log('图片已预加载完成,可以立即使用');
    // 将图片添加到DOM
    document.getElementById('container').appendChild(img);
  })
  .catch(error => {
    console.error('预加载失败:', error);
  });

2. 多张图片预加载

function preloadImages(urls) {
  // 创建所有图片的Promise数组
  const promises = urls.map(url => {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve({url, img, status: 'success'});
      img.onerror = () => resolve({url, img: null, status: 'error'});
      img.src = url;
    });
  });

  return Promise.all(promises);
}

// 使用示例
const imageUrls = [
  'image1.jpg',
  'image2.jpg',
  'image3.jpg',
  'image4.jpg'
];

preloadImages(imageUrls)
  .then(results => {
    const successCount = results.filter(r => r.status === 'success').length;
    const errorCount = results.filter(r => r.status === 'error').length;

    console.log(`预加载完成: ${successCount}张成功, ${errorCount}张失败`);

    // 可以根据results在需要时快速使用图片
    results.forEach(result => {
      if (result.status === 'success') {
        // 图片已缓存,可随时使用
      }
    });
  });

四、完整实战示例

下面是一个完整的、可直接运行的图片预加载实现,包含进度显示和错误处理:

<!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>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Arial, sans-serif;
            line-height: 1.6;
            padding: 20px;
            background-color: #f5f7fa;
            color: #333;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
        }

        header {
            text-align: center;
            margin-bottom: 40px;
            padding-bottom: 20px;
            border-bottom: 2px solid #eaeaea;
        }

        h1 {
            color: #2c3e50;
            margin-bottom: 10px;
        }

        .subtitle {
            color: #7f8c8d;
            font-size: 1.1rem;
        }

        .demo-section {
            display: flex;
            flex-wrap: wrap;
            gap: 30px;
            margin-bottom: 50px;
        }

        .controls {
            flex: 1;
            min-width: 300px;
            background: white;
            padding: 25px;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
        }

        .gallery {
            flex: 2;
            min-width: 300px;
            background: white;
            padding: 25px;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
        }

        h2 {
            color: #3498db;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 1px solid #eee;
        }

        .url-list {
            margin-bottom: 20px;
        }

        textarea {
            width: 100%;
            height: 150px;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-family: monospace;
            font-size: 14px;
            resize: vertical;
        }

        .btn {
            background-color: #3498db;
            color: white;
            border: none;
            padding: 12px 25px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            font-weight: 600;
            transition: background-color 0.3s;
            margin-right: 10px;
            margin-bottom: 10px;
        }

        .btn:hover {
            background-color: #2980b9;
        }

        .btn:disabled {
            background-color: #95a5a6;
            cursor: not-allowed;
        }

        .btn-secondary {
            background-color: #2ecc71;
        }

        .btn-secondary:hover {
            background-color: #27ae60;
        }

        .progress-container {
            margin-top: 20px;
            background-color: #ecf0f1;
            border-radius: 5px;
            overflow: hidden;
        }

        .progress-bar {
            height: 30px;
            background-color: #3498db;
            width: 0%;
            transition: width 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
        }

        .status {
            margin-top: 15px;
            padding: 10px;
            border-radius: 5px;
            font-size: 14px;
        }

        .status.success {
            background-color: #d5edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .status.error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .gallery-images {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 15px;
            margin-top: 20px;
        }

        .image-item {
            position: relative;
            border-radius: 5px;
            overflow: hidden;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
            height: 200px;
            background-color: #f8f9fa;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .image-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            transition: transform 0.3s;
        }

        .image-item img:hover {
            transform: scale(1.05);
        }

        .placeholder {
            color: #95a5a6;
            font-style: italic;
        }

        .error-badge {
            position: absolute;
            top: 10px;
            right: 10px;
            background-color: #e74c3c;
            color: white;
            padding: 3px 8px;
            border-radius: 3px;
            font-size: 12px;
        }

        .stats {
            margin-top: 20px;
            padding: 15px;
            background-color: #f8f9fa;
            border-radius: 5px;
            font-size: 14px;
        }

        footer {
            text-align: center;
            margin-top: 50px;
            padding-top: 20px;
            border-top: 1px solid #eee;
            color: #7f8c8d;
            font-size: 14px;
        }

        .code-block {
            background-color: #2c3e50;
            color: #ecf0f1;
            padding: 15px;
            border-radius: 5px;
            font-family: monospace;
            font-size: 14px;
            margin-top: 20px;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>JavaScript图片预加载示例</h1>
            <p class="subtitle">告别图片加载卡顿,提升用户体验</p>
        </header>

        <div class="demo-section">
            <div class="controls">
                <h2>预加载控制面板</h2>

                <div class="url-list">
                    <label for="imageUrls">图片URL列表(每行一个URL):</label>
                    <textarea id="imageUrls" placeholder="请输入图片URL,每行一个">https://images.unsplash.com/photo-1506744038136-46273834b3fb
https://images.unsplash.com/photo-1519681393784-d120267933ba
https://images.unsplash.com/photo-1501785888041-af3ef285b470
https://images.unsplash.com/photo-1475924156734-496f6cac6ec1
https://images.unsplash.com/photo-1439853949127-fa647821eba0
https://images.unsplash.com/photo-1501785888041-af3ef285b470
https://images.unsplash.com/photo-1506260408121-e353d10b87c7
https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05</textarea>
                </div>

                <button id="preloadBtn" class="btn">开始预加载</button>
                <button id="showBtn" class="btn btn-secondary">显示所有图片</button>
                <button id="resetBtn" class="btn">重置</button>

                <div class="progress-container">
                    <div id="progressBar" class="progress-bar">0%</div>
                </div>

                <div id="status" class="status"></div>

                <div class="stats">
                    <div>总图片数: <span id="totalCount">0</span></div>
                    <div>已加载: <span id="loadedCount">0</span></div>
                    <div>失败: <span id="errorCount">0</span></div>
                    <div>缓存大小: <span id="cacheSize">0</span> 张</div>
                </div>
            </div>

            <div class="gallery">
                <h2>图片画廊</h2>
                <p>预加载完成后,点击"显示所有图片"按钮查看效果</p>

                <div id="galleryContainer" class="gallery-images">
                    <div class="image-item placeholder">
                        图片将在这里显示
                    </div>
                </div>

                <div class="code-block">
                    // 预加载核心代码<br>
                    function preloadImages(urls) {<br>
                    &nbsp;&nbsp;return urls.map(url => {<br>
                    &nbsp;&nbsp;&nbsp;&nbsp;return new Promise((resolve) => {<br>
                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const img = new Image();<br>
                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;img.onload = () => resolve({url, status: 'loaded'});<br>
                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;img.onerror = () => resolve({url, status: 'error'});<br>
                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;img.src = url;<br>
                    &nbsp;&nbsp;&nbsp;&nbsp;});<br>
                    &nbsp;&nbsp;});<br>
                    }
                </div>
            </div>
        </div>

        <footer>
            <p>© 2023 图片预加载示例 | 使用JavaScript预加载图片可以显著提升用户体验</p>
        </footer>
    </div>

    <script>
        class ImagePreloader {
            constructor() {
                this.cache = new Map();
                this.loadedCount = 0;
                this.errorCount = 0;
                this.totalCount = 0;
                this.imageElements = new Map();

                // 绑定事件
                document.getElementById('preloadBtn').addEventListener('click', () => this.startPreloading());
                document.getElementById('showBtn').addEventListener('click', () => this.displayImages());
                document.getElementById('resetBtn').addEventListener('click', () => this.reset());

                // 初始化统计
                this.updateStats();
            }

            // 获取URL列表
            getUrls() {
                const textarea = document.getElementById('imageUrls');
                return textarea.value
                    .split('\n')
                    .map(url => url.trim())
                    .filter(url => url.length > 0);
            }

            // 开始预加载
            async startPreloading() {
                const urls = this.getUrls();
                if (urls.length === 0) {
                    this.showStatus('请输入至少一个图片URL', 'error');
                    return;
                }

                // 重置状态
                this.reset();
                this.totalCount = urls.length;
                this.updateStats();

                // 禁用按钮
                document.getElementById('preloadBtn').disabled = true;
                this.showStatus('开始预加载...', '');

                // 创建所有图片的Promise
                const promises = urls.map(url => this.preloadSingleImage(url));

                // 使用Promise.allSettled等待所有图片加载完成(无论成功或失败)
                const results = await Promise.allSettled(promises);

                // 处理结果
                results.forEach(result => {
                    if (result.status === 'fulfilled') {
                        const { url, status } = result.value;
                        if (status === 'loaded') {
                            this.loadedCount++;
                        } else {
                            this.errorCount++;
                        }
                    }

                    // 更新进度
                    const progress = ((this.loadedCount + this.errorCount) / this.totalCount) * 100;
                    this.updateProgress(progress);
                    this.updateStats();
                });

                // 完成
                this.showStatus(`预加载完成!成功加载 ${this.loadedCount} 张,失败 ${this.errorCount} 张`, 'success');
                document.getElementById('preloadBtn').disabled = false;

                // 如果全部成功,启用显示按钮
                if (this.loadedCount > 0) {
                    document.getElementById('showBtn').disabled = false;
                }
            }

            // 预加载单张图片
            preloadSingleImage(url) {
                return new Promise((resolve) => {
                    // 如果已经缓存,直接返回
                    if (this.cache.has(url)) {
                        resolve({ url, status: 'loaded' });
                        return;
                    }

                    const img = new Image();

                    img.onload = () => {
                        // 缓存图片对象
                        this.cache.set(url, img);
                        resolve({ url, status: 'loaded' });
                    };

                    img.onerror = () => {
                        console.error(`图片加载失败: ${url}`);
                        resolve({ url, status: 'error' });
                    };

                    // 开始加载
                    img.src = url;
                });
            }

            // 显示所有图片
            displayImages() {
                const gallery = document.getElementById('galleryContainer');
                gallery.innerHTML = '';

                const urls = this.getUrls();

                if (urls.length === 0) {
                    gallery.innerHTML = '<div class="placeholder">没有图片可显示</div>';
                    return;
                }

                urls.forEach(url => {
                    const container = document.createElement('div');
                    container.className = 'image-item';

                    if (this.cache.has(url)) {
                        // 从缓存中获取图片
                        const img = this.cache.get(url).cloneNode();
                        container.appendChild(img);
                    } else {
                        // 如果未缓存,实时加载
                        const img = new Image();
                        img.src = url;
                        img.onerror = () => {
                            const errorBadge = document.createElement('div');
                            errorBadge.className = 'error-badge';
                            errorBadge.textContent = '加载失败';
                            container.appendChild(errorBadge);
                        };
                        container.appendChild(img);
                    }

                    gallery.appendChild(container);
                });

                this.showStatus('图片已显示!已缓存的图片会立即显示,未缓存的图片需要重新加载。', 'success');
            }

            // 更新进度条
            updateProgress(percent) {
                const progressBar = document.getElementById('progressBar');
                progressBar.style.width = `${percent}%`;
                progressBar.textContent = `${Math.round(percent)}%`;
            }

            // 更新统计信息
            updateStats() {
                document.getElementById('totalCount').textContent = this.totalCount;
                document.getElementById('loadedCount').textContent = this.loadedCount;
                document.getElementById('errorCount').textContent = this.errorCount;
                document.getElementById('cacheSize').textContent = this.cache.size;
            }

            // 显示状态消息
            showStatus(message, type) {
                const statusEl = document.getElementById('status');
                statusEl.textContent = message;
                statusEl.className = 'status';
                if (type) {
                    statusEl.classList.add(type);
                }
            }

            // 重置
            reset() {
                this.cache.clear();
                this.loadedCount = 0;
                this.errorCount = 0;
                this.totalCount = 0;

                this.updateProgress(0);
                this.updateStats();
                this.showStatus('', '');

                document.getElementById('preloadBtn').disabled = false;
                document.getElementById('showBtn').disabled = true;

                const gallery = document.getElementById('galleryContainer');
                gallery.innerHTML = '<div class="image-item placeholder">图片将在这里显示</div>';
            }
        }

        // 初始化预加载器
        const preloader = new ImagePreloader();
    </script>
</body>
</html>

五、高级技巧与优化

1. 分批加载大量图片

当有大量图片需要预加载时,可以分批进行,避免同时发起过多请求:

async function preloadBatch(urls, batchSize = 5) {
  const results = [];

  for (let i = 0; i < urls.length; i += batchSize) {
    const batch = urls.slice(i, i + batchSize);
    console.log(`加载批次 ${i/batchSize + 1}/${Math.ceil(urls.length/batchSize)}`);

    const batchResults = await Promise.allSettled(
      batch.map(url => preloadImage(url))
    );

    results.push(...batchResults);

    // 可选的延迟,避免过于密集的请求
    // await new Promise(resolve => setTimeout(resolve, 100));
  }

  return results;
}

2. 结合懒加载

预加载首屏图片,其他图片使用懒加载:

class AdvancedImageLoader {
  constructor() {
    this.preloaded = new Set();
    this.lazyImages = new Map();
  }

  // 预加载关键图片
  preloadCritical(images) {
    return Promise.allSettled(
      images.map(img => this.preload(img.url))
    );
  }

  // 懒加载图片
  lazyLoad(imageElement, url) {
    if (this.preloaded.has(url)) {
      // 已预加载,直接设置
      imageElement.src = url;
      return Promise.resolve();
    }

    // 否则进行懒加载
    return this.preload(url).then(() => {
      imageElement.src = url;
    });
  }

  // 预加载单张图片
  preload(url) {
    if (this.preloaded.has(url)) {
      return Promise.resolve();
    }

    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => {
        this.preloaded.add(url);
        resolve(url);
      };
      img.onerror = reject;
      img.src = url;
    });
  }
}

3. 使用Intersection Observer API实现智能预加载

function intelligentPreload(images, options = {}) {
  const { rootMargin = '200px 0px' } = options;

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

        if (src && !img.src) {
          // 如果图片在视口附近,开始预加载
          preloadImage(src).then(() => {
            img.src = src;
            img.classList.add('loaded');
          });

          observer.unobserve(img);
        }
      }
    });
  }, { rootMargin });

  // 观察所有图片元素
  images.forEach(img => observer.observe(img));
}

六、注意事项

性能平衡:不要过度预加载,只预加载用户可能查看的图片 移动端考虑:注意移动设备的网络状况和流量限制 内存管理:大量图片预加载可能占用大量内存,适当清理不用的图片 CDN优化:使用CDN和适当格式(WebP)可以进一步提升加载速度 错误处理:始终添加错误处理,避免因单张图片加载失败影响整体功能

七、总结

通过图片预加载,我们可以显著提升网站的图片加载体验,减少卡顿和空白区域的出现。实现方法从简单的单张图片预加载到复杂的批量智能预加载,可以根据实际需求选择合适方案。

在实际项目中,建议结合预加载、懒加载和响应式图片技术,为用户提供最优的图片加载体验。

现在你可以将这些技术应用到你的项目中,让图片加载更加流畅!

相关推荐