Skip to content

图片优化2024:Vue.js开发者掌握现代图片性能优化完整指南

📊 SEO元描述:2024年最新Vue图片优化教程,详解WebP、AVIF格式、懒加载、响应式图片。包含完整优化方案,适合Vue.js开发者快速掌握图片性能优化技术。

核心关键词:Vue图片优化2024、WebP格式、图片懒加载、响应式图片、Vue图片性能

长尾关键词:Vue图片懒加载怎么实现、WebP格式如何使用、响应式图片最佳实践、图片压缩优化策略、前端图片性能优化


📚 图片优化学习目标与核心收获

通过本节Vue图片优化深度教程,你将系统性掌握:

  • 现代图片格式:深入理解WebP、AVIF等现代图片格式的优势和应用
  • 图片懒加载技术:掌握Intersection Observer和虚拟滚动的图片懒加载
  • 响应式图片方案:学会使用srcset和picture元素实现响应式图片
  • 图片压缩优化:掌握自动化图片压缩和优化工具链
  • CDN集成策略:学会集成图片CDN和实时处理服务
  • 性能监控体系:建立图片加载性能监控和优化体系

🎯 适合人群

  • Vue.js中高级开发者需要优化应用中的图片加载性能
  • 前端性能工程师专注于提升Web应用的视觉加载体验
  • UI/UX开发者需要平衡视觉效果和加载性能
  • 移动端开发者面临网络环境复杂的图片优化挑战

🌟 为什么图片优化如此重要?如何制定优化策略?

为什么图片优化如此重要?这是现代Web性能优化的关键问题。图片通常占据网页总大小的60-70%,是影响加载速度的主要因素。合理的图片优化策略能够显著提升用户体验,特别是在移动设备和慢速网络环境下,也是现代Vue应用的必备技术。

图片优化的核心价值

  • 🎯 加载速度提升:减少图片大小,显著提升页面加载速度
  • 🔧 带宽使用优化:降低网络传输成本,特别是移动端流量
  • 💡 用户体验改善:更快的图片显示,减少用户等待时间
  • 📚 SEO性能提升:更好的Core Web Vitals指标,提升搜索排名
  • 🚀 设备适配优化:针对不同设备和屏幕提供最适合的图片

💡 策略建议:根据图片用途、设备类型和网络环境制定差异化的优化策略,平衡视觉质量和加载性能

现代图片格式应用

现代图片格式提供更好的压缩率和质量:

vue
<template>
  <div class="image-format-demo">
    <div class="demo-section">
      <h3>现代图片格式演示</h3>
      
      <!-- 格式对比 -->
      <div class="format-comparison">
        <h4>格式对比</h4>
        <div class="comparison-grid">
          <div 
            v-for="format in imageFormats" 
            :key="format.name"
            class="format-card"
          >
            <div class="format-header">
              <h5>{{ format.name }}</h5>
              <div class="format-support" :class="format.support">
                {{ format.supportText }}
              </div>
            </div>
            
            <div class="format-image">
              <picture>
                <source 
                  v-if="format.name === 'AVIF'" 
                  :srcset="format.src" 
                  type="image/avif"
                >
                <source 
                  v-if="format.name === 'WebP'" 
                  :srcset="format.src" 
                  type="image/webp"
                >
                <img 
                  :src="format.fallback" 
                  :alt="format.name + ' example'"
                  @load="onImageLoad(format.name)"
                  @error="onImageError(format.name)"
                >
              </picture>
            </div>
            
            <div class="format-stats">
              <div class="stat-item">
                <span class="stat-label">文件大小:</span>
                <span class="stat-value">{{ format.size }}KB</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">压缩率:</span>
                <span class="stat-value">{{ format.compression }}%</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">质量:</span>
                <span class="stat-value">{{ format.quality }}/10</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      
      <!-- 自适应图片 -->
      <div class="adaptive-images">
        <h4>自适应图片</h4>
        <div class="adaptive-controls">
          <label>
            设备类型:
            <select v-model="selectedDevice" @change="updateAdaptiveImages">
              <option value="mobile">移动设备</option>
              <option value="tablet">平板设备</option>
              <option value="desktop">桌面设备</option>
            </select>
          </label>
          <label>
            网络状况:
            <select v-model="networkCondition" @change="updateAdaptiveImages">
              <option value="slow">慢速网络</option>
              <option value="fast">快速网络</option>
              <option value="wifi">WiFi</option>
            </select>
          </label>
        </div>
        
        <div class="adaptive-showcase">
          <div class="adaptive-image-container">
            <picture>
              <source 
                :srcset="adaptiveImage.avif" 
                type="image/avif"
                :media="adaptiveImage.media"
              >
              <source 
                :srcset="adaptiveImage.webp" 
                type="image/webp"
                :media="adaptiveImage.media"
              >
              <img 
                :src="adaptiveImage.fallback"
                :alt="adaptiveImage.alt"
                class="adaptive-img"
                @load="onAdaptiveImageLoad"
              >
            </picture>
          </div>
          
          <div class="adaptive-info">
            <h5>当前配置</h5>
            <div class="info-grid">
              <div class="info-item">
                <span class="info-label">设备:</span>
                <span class="info-value">{{ selectedDevice }}</span>
              </div>
              <div class="info-item">
                <span class="info-label">网络:</span>
                <span class="info-value">{{ networkCondition }}</span>
              </div>
              <div class="info-item">
                <span class="info-label">选择格式:</span>
                <span class="info-value">{{ adaptiveImage.selectedFormat }}</span>
              </div>
              <div class="info-item">
                <span class="info-label">预计大小:</span>
                <span class="info-value">{{ adaptiveImage.estimatedSize }}KB</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      
      <!-- 响应式图片 -->
      <div class="responsive-images">
        <h4>响应式图片</h4>
        <div class="responsive-showcase">
          <div class="responsive-container">
            <img 
              :srcset="responsiveImage.srcset"
              :sizes="responsiveImage.sizes"
              :src="responsiveImage.fallback"
              alt="响应式图片示例"
              class="responsive-img"
              @load="onResponsiveImageLoad"
            >
          </div>
          
          <div class="responsive-info">
            <h5>响应式配置</h5>
            <div class="srcset-info">
              <div class="srcset-item">
                <span class="srcset-size">320w:</span>
                <span class="srcset-desc">移动设备小屏</span>
              </div>
              <div class="srcset-item">
                <span class="srcset-size">768w:</span>
                <span class="srcset-desc">平板设备</span>
              </div>
              <div class="srcset-item">
                <span class="srcset-size">1200w:</span>
                <span class="srcset-desc">桌面设备</span>
              </div>
              <div class="srcset-item">
                <span class="srcset-size">1920w:</span>
                <span class="srcset-desc">高分辨率屏幕</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      
      <!-- 图片性能监控 -->
      <div class="image-performance">
        <h4>图片性能监控</h4>
        <div class="performance-stats">
          <div class="stat-card">
            <div class="stat-title">总加载时间</div>
            <div class="stat-value">{{ imagePerformance.totalLoadTime }}ms</div>
            <div class="stat-trend" :class="imagePerformance.loadTimeTrend">
              {{ imagePerformance.loadTimeTrend === 'up' ? '📈' : '📉' }}
            </div>
          </div>
          
          <div class="stat-card">
            <div class="stat-title">平均文件大小</div>
            <div class="stat-value">{{ imagePerformance.averageSize }}KB</div>
            <div class="stat-trend" :class="imagePerformance.sizeTrend">
              {{ imagePerformance.sizeTrend === 'down' ? '📉' : '📈' }}
            </div>
          </div>
          
          <div class="stat-card">
            <div class="stat-title">现代格式使用率</div>
            <div class="stat-value">{{ imagePerformance.modernFormatUsage }}%</div>
            <div class="stat-trend up">📈</div>
          </div>
          
          <div class="stat-card">
            <div class="stat-title">懒加载命中率</div>
            <div class="stat-value">{{ imagePerformance.lazyLoadHitRate }}%</div>
            <div class="stat-trend up">📈</div>
          </div>
        </div>
        
        <div class="performance-timeline">
          <h5>加载时间线</h5>
          <div class="timeline-container">
            <div 
              v-for="(load, index) in imageLoadHistory" 
              :key="index"
              class="timeline-item"
            >
              <div class="timeline-time">{{ load.time }}ms</div>
              <div class="timeline-format">{{ load.format }}</div>
              <div class="timeline-size">{{ load.size }}KB</div>
              <div class="timeline-bar">
                <div 
                  class="timeline-fill"
                  :style="{ width: getTimelineWidth(load.time) + '%' }"
                ></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ImageFormatDemo',
  data() {
    return {
      imageFormats: [
        {
          name: 'JPEG',
          src: '/images/sample.jpg',
          fallback: '/images/sample.jpg',
          size: 120,
          compression: 0,
          quality: 7,
          support: 'full',
          supportText: '完全支持'
        },
        {
          name: 'WebP',
          src: '/images/sample.webp',
          fallback: '/images/sample.jpg',
          size: 85,
          compression: 29,
          quality: 8,
          support: 'good',
          supportText: '良好支持'
        },
        {
          name: 'AVIF',
          src: '/images/sample.avif',
          fallback: '/images/sample.jpg',
          size: 65,
          compression: 46,
          quality: 9,
          support: 'limited',
          supportText: '有限支持'
        }
      ],
      
      selectedDevice: 'desktop',
      networkCondition: 'fast',
      
      adaptiveImage: {
        avif: '/images/adaptive-desktop.avif',
        webp: '/images/adaptive-desktop.webp',
        fallback: '/images/adaptive-desktop.jpg',
        media: '(min-width: 1024px)',
        alt: '自适应图片示例',
        selectedFormat: 'WebP',
        estimatedSize: 150
      },
      
      responsiveImage: {
        srcset: '/images/responsive-320.webp 320w, /images/responsive-768.webp 768w, /images/responsive-1200.webp 1200w, /images/responsive-1920.webp 1920w',
        sizes: '(max-width: 320px) 280px, (max-width: 768px) 720px, (max-width: 1200px) 1120px, 1920px',
        fallback: '/images/responsive-1200.jpg'
      },
      
      imagePerformance: {
        totalLoadTime: 1250,
        averageSize: 95,
        modernFormatUsage: 75,
        lazyLoadHitRate: 85,
        loadTimeTrend: 'down',
        sizeTrend: 'down'
      },
      
      imageLoadHistory: [
        { time: 120, format: 'WebP', size: 85 },
        { time: 95, format: 'AVIF', size: 65 },
        { time: 180, format: 'JPEG', size: 120 },
        { time: 110, format: 'WebP', size: 90 },
        { time: 85, format: 'AVIF', size: 60 }
      ]
    }
  },
  
  mounted() {
    this.detectFormatSupport()
    this.updateAdaptiveImages()
  },
  
  methods: {
    detectFormatSupport() {
      // 检测浏览器对现代图片格式的支持
      this.checkWebPSupport()
      this.checkAVIFSupport()
    },
    
    checkWebPSupport() {
      const canvas = document.createElement('canvas')
      canvas.width = 1
      canvas.height = 1
      const webpSupported = canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0
      
      const webpFormat = this.imageFormats.find(f => f.name === 'WebP')
      if (webpFormat) {
        webpFormat.support = webpSupported ? 'good' : 'none'
        webpFormat.supportText = webpSupported ? '支持' : '不支持'
      }
    },
    
    checkAVIFSupport() {
      // 简化的AVIF支持检测
      const avifSupported = 'createImageBitmap' in window
      
      const avifFormat = this.imageFormats.find(f => f.name === 'AVIF')
      if (avifFormat) {
        avifFormat.support = avifSupported ? 'limited' : 'none'
        avifFormat.supportText = avifSupported ? '有限支持' : '不支持'
      }
    },
    
    updateAdaptiveImages() {
      const configs = {
        mobile: {
          slow: { format: 'JPEG', size: 45, quality: 'low' },
          fast: { format: 'WebP', size: 65, quality: 'medium' },
          wifi: { format: 'AVIF', size: 50, quality: 'high' }
        },
        tablet: {
          slow: { format: 'WebP', size: 85, quality: 'medium' },
          fast: { format: 'AVIF', size: 70, quality: 'high' },
          wifi: { format: 'AVIF', size: 75, quality: 'high' }
        },
        desktop: {
          slow: { format: 'WebP', size: 120, quality: 'medium' },
          fast: { format: 'AVIF', size: 95, quality: 'high' },
          wifi: { format: 'AVIF', size: 110, quality: 'high' }
        }
      }
      
      const config = configs[this.selectedDevice][this.networkCondition]
      
      this.adaptiveImage = {
        ...this.adaptiveImage,
        selectedFormat: config.format,
        estimatedSize: config.size,
        avif: `/images/adaptive-${this.selectedDevice}-${config.quality}.avif`,
        webp: `/images/adaptive-${this.selectedDevice}-${config.quality}.webp`,
        fallback: `/images/adaptive-${this.selectedDevice}-${config.quality}.jpg`
      }
    },
    
    onImageLoad(format) {
      console.log(`${format} image loaded successfully`)
      this.recordImageLoad(format)
    },
    
    onImageError(format) {
      console.error(`Failed to load ${format} image`)
    },
    
    onAdaptiveImageLoad() {
      console.log('Adaptive image loaded')
      this.recordImageLoad(this.adaptiveImage.selectedFormat)
    },
    
    onResponsiveImageLoad() {
      console.log('Responsive image loaded')
      this.recordImageLoad('Responsive')
    },
    
    recordImageLoad(format) {
      const loadTime = Math.round(50 + Math.random() * 100)
      const size = Math.round(60 + Math.random() * 60)
      
      this.imageLoadHistory.unshift({
        time: loadTime,
        format,
        size,
        timestamp: Date.now()
      })
      
      // 保持历史记录在合理范围内
      if (this.imageLoadHistory.length > 10) {
        this.imageLoadHistory.pop()
      }
      
      // 更新性能指标
      this.updatePerformanceMetrics()
    },
    
    updatePerformanceMetrics() {
      const recent = this.imageLoadHistory.slice(0, 5)
      const avgLoadTime = recent.reduce((sum, item) => sum + item.time, 0) / recent.length
      const avgSize = recent.reduce((sum, item) => sum + item.size, 0) / recent.length
      
      this.imagePerformance.totalLoadTime = Math.round(avgLoadTime)
      this.imagePerformance.averageSize = Math.round(avgSize)
      
      // 计算现代格式使用率
      const modernFormats = recent.filter(item => ['WebP', 'AVIF'].includes(item.format))
      this.imagePerformance.modernFormatUsage = Math.round((modernFormats.length / recent.length) * 100)
    },
    
    getTimelineWidth(time) {
      const maxTime = Math.max(...this.imageLoadHistory.map(item => item.time))
      return (time / maxTime) * 100
    }
  }
}
</script>

<style scoped>
.demo-section {
  padding: 24px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.format-comparison,
.adaptive-images,
.responsive-images,
.image-performance {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #e9ecef;
  border-radius: 8px;
}

.comparison-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin-top: 16px;
}

.format-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.2s;
}

.format-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.format-header {
  padding: 16px;
  background: #f8f9fa;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.format-header h5 {
  margin: 0;
  font-size: 18px;
  font-weight: bold;
}

.format-support {
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
}

.format-support.full {
  background: #d4edda;
  color: #155724;
}

.format-support.good {
  background: #d1ecf1;
  color: #0c5460;
}

.format-support.limited {
  background: #fff3cd;
  color: #856404;
}

.format-support.none {
  background: #f8d7da;
  color: #721c24;
}

.format-image {
  height: 150px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f8f9fa;
}

.format-image img {
  max-width: 100%;
  max-height: 100%;
  object-fit: cover;
}

.format-stats {
  padding: 16px;
}

.stat-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 8px;
}

.stat-label {
  font-size: 14px;
  color: #666;
}

.stat-value {
  font-weight: bold;
  color: #007bff;
}

.adaptive-controls {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.adaptive-controls label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
}

.adaptive-controls select {
  padding: 4px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.adaptive-showcase,
.responsive-showcase {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  align-items: start;
}

.adaptive-image-container,
.responsive-container {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.adaptive-img,
.responsive-img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.adaptive-info,
.responsive-info {
  padding: 16px;
  background: #f8f9fa;
  border-radius: 8px;
}

.info-grid {
  display: grid;
  gap: 8px;
  margin-top: 12px;
}

.info-item {
  display: flex;
  justify-content: space-between;
}

.info-label {
  font-size: 14px;
  color: #666;
}

.info-value {
  font-weight: bold;
  color: #007bff;
}

.srcset-info {
  margin-top: 12px;
}

.srcset-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 6px;
  font-size: 14px;
}

.srcset-size {
  font-family: monospace;
  color: #007bff;
  font-weight: bold;
}

.srcset-desc {
  color: #666;
}

.performance-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
  margin-bottom: 20px;
}

.stat-card {
  padding: 16px;
  background: #f8f9fa;
  border-radius: 8px;
  text-align: center;
  position: relative;
}

.stat-title {
  font-size: 14px;
  color: #666;
  margin-bottom: 8px;
}

.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #007bff;
  margin-bottom: 4px;
}

.stat-trend {
  position: absolute;
  top: 8px;
  right: 8px;
  font-size: 16px;
}

.stat-trend.up {
  color: #28a745;
}

.stat-trend.down {
  color: #dc3545;
}

.timeline-container {
  max-height: 200px;
  overflow-y: auto;
}

.timeline-item {
  display: grid;
  grid-template-columns: 80px 80px 80px 1fr;
  gap: 12px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}

.timeline-time {
  font-family: monospace;
  font-size: 12px;
  color: #666;
}

.timeline-format {
  font-size: 12px;
  font-weight: bold;
  color: #007bff;
}

.timeline-size {
  font-size: 12px;
  color: #666;
}

.timeline-bar {
  height: 8px;
  background: #e9ecef;
  border-radius: 4px;
  overflow: hidden;
}

.timeline-fill {
  height: 100%;
  background: linear-gradient(90deg, #007bff, #0056b3);
  transition: width 0.3s ease;
}
</style>

现代图片格式核心优势

  • WebP格式:相比JPEG减少25-35%文件大小,支持透明度
  • AVIF格式:相比JPEG减少50%文件大小,更好的压缩算法
  • 渐进式加载:支持渐进式显示,改善用户体验
  • 浏览器兼容:通过picture元素实现优雅降级

图片懒加载实现

什么是图片懒加载?如何实现高性能的懒加载?

图片懒加载是指只在图片进入可视区域时才开始加载,显著减少初始页面加载时间:

vue
<!-- components/LazyImage.vue - 高性能懒加载组件 -->
<template>
  <div 
    class="lazy-image-container"
    :class="{ 'is-loading': isLoading, 'is-loaded': isLoaded, 'is-error': hasError }"
    ref="container"
  >
    <!-- 占位符 -->
    <div v-if="!isLoaded && !hasError" class="image-placeholder">
      <div v-if="showSkeleton" class="skeleton-loader">
        <div class="skeleton-shimmer"></div>
      </div>
      <div v-else class="placeholder-content">
        <div class="placeholder-icon">🖼️</div>
        <div class="placeholder-text">{{ placeholderText }}</div>
      </div>
    </div>
    
    <!-- 实际图片 -->
    <picture v-show="isLoaded">
      <source 
        v-if="avifSrc" 
        :srcset="avifSrc" 
        type="image/avif"
      >
      <source 
        v-if="webpSrc" 
        :srcset="webpSrc" 
        type="image/webp"
      >
      <img 
        ref="image"
        :src="actualSrc"
        :alt="alt"
        :loading="nativeLoading ? 'lazy' : 'eager'"
        @load="onImageLoad"
        @error="onImageError"
        class="lazy-image"
      >
    </picture>
    
    <!-- 错误状态 -->
    <div v-if="hasError" class="error-placeholder">
      <div class="error-icon">❌</div>
      <div class="error-text">图片加载失败</div>
      <button @click="retry" class="retry-button">重试</button>
    </div>
    
    <!-- 加载进度 -->
    <div v-if="showProgress && isLoading" class="loading-progress">
      <div class="progress-bar">
        <div 
          class="progress-fill"
          :style="{ width: loadingProgress + '%' }"
        ></div>
      </div>
      <div class="progress-text">{{ loadingProgress }}%</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'LazyImage',
  props: {
    src: {
      type: String,
      required: true
    },
    webpSrc: {
      type: String,
      default: ''
    },
    avifSrc: {
      type: String,
      default: ''
    },
    alt: {
      type: String,
      default: ''
    },
    placeholderText: {
      type: String,
      default: '加载中...'
    },
    showSkeleton: {
      type: Boolean,
      default: true
    },
    showProgress: {
      type: Boolean,
      default: false
    },
    rootMargin: {
      type: String,
      default: '50px'
    },
    threshold: {
      type: Number,
      default: 0.1
    },
    nativeLoading: {
      type: Boolean,
      default: false
    },
    retryCount: {
      type: Number,
      default: 3
    }
  },
  
  data() {
    return {
      isLoading: false,
      isLoaded: false,
      hasError: false,
      actualSrc: '',
      loadingProgress: 0,
      observer: null,
      currentRetryCount: 0
    }
  },
  
  mounted() {
    if (this.nativeLoading) {
      // 使用原生懒加载
      this.loadImage()
    } else {
      // 使用Intersection Observer
      this.setupIntersectionObserver()
    }
  },
  
  beforeUnmount() {
    if (this.observer) {
      this.observer.disconnect()
    }
  },
  
  methods: {
    setupIntersectionObserver() {
      if (!('IntersectionObserver' in window)) {
        // 降级到立即加载
        this.loadImage()
        return
      }
      
      this.observer = new IntersectionObserver(
        (entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              this.loadImage()
              this.observer.unobserve(entry.target)
            }
          })
        },
        {
          rootMargin: this.rootMargin,
          threshold: this.threshold
        }
      )
      
      this.observer.observe(this.$refs.container)
    },
    
    async loadImage() {
      if (this.isLoading || this.isLoaded) return
      
      this.isLoading = true
      this.hasError = false
      this.loadingProgress = 0
      
      try {
        // 选择最佳图片源
        const bestSrc = await this.selectBestImageSource()
        
        // 预加载图片
        await this.preloadImage(bestSrc)
        
        this.actualSrc = bestSrc
        this.isLoaded = true
        this.isLoading = false
        this.loadingProgress = 100
        
        this.$emit('load', {
          src: bestSrc,
          loadTime: Date.now() - this.loadStartTime
        })
        
      } catch (error) {
        this.handleImageError(error)
      }
    },
    
    async selectBestImageSource() {
      // 检测浏览器支持和网络状况
      const supportsAVIF = await this.checkAVIFSupport()
      const supportsWebP = await this.checkWebPSupport()
      const networkSpeed = this.getNetworkSpeed()
      
      // 根据支持情况和网络状况选择最佳源
      if (supportsAVIF && this.avifSrc && networkSpeed !== 'slow') {
        return this.avifSrc
      } else if (supportsWebP && this.webpSrc) {
        return this.webpSrc
      } else {
        return this.src
      }
    },
    
    async checkAVIFSupport() {
      // 简化的AVIF支持检测
      return new Promise(resolve => {
        const avif = new Image()
        avif.onload = () => resolve(true)
        avif.onerror = () => resolve(false)
        avif.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgABogQEAwgMg8f8D///8WfhwB8+ErK42A='
      })
    },
    
    async checkWebPSupport() {
      return new Promise(resolve => {
        const webp = new Image()
        webp.onload = () => resolve(true)
        webp.onerror = () => resolve(false)
        webp.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'
      })
    },
    
    getNetworkSpeed() {
      // 简化的网络速度检测
      if ('connection' in navigator) {
        const connection = navigator.connection
        if (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g') {
          return 'slow'
        } else if (connection.effectiveType === '3g') {
          return 'medium'
        } else {
          return 'fast'
        }
      }
      return 'unknown'
    },
    
    preloadImage(src) {
      return new Promise((resolve, reject) => {
        this.loadStartTime = Date.now()
        
        const img = new Image()
        
        // 模拟加载进度
        if (this.showProgress) {
          this.simulateLoadingProgress()
        }
        
        img.onload = () => {
          this.loadingProgress = 100
          resolve(img)
        }
        
        img.onerror = () => {
          reject(new Error(`Failed to load image: ${src}`))
        }
        
        img.src = src
      })
    },
    
    simulateLoadingProgress() {
      const interval = setInterval(() => {
        this.loadingProgress += Math.random() * 20
        if (this.loadingProgress >= 90) {
          this.loadingProgress = 90
          clearInterval(interval)
        }
      }, 100)
    },
    
    onImageLoad() {
      // 图片加载完成的额外处理
      this.$emit('loaded')
    },
    
    onImageError() {
      this.handleImageError(new Error('Image load failed'))
    },
    
    handleImageError(error) {
      this.isLoading = false
      this.hasError = true
      this.currentRetryCount++
      
      console.error('Image loading error:', error)
      
      this.$emit('error', {
        error,
        retryCount: this.currentRetryCount,
        src: this.actualSrc || this.src
      })
    },
    
    retry() {
      if (this.currentRetryCount < this.retryCount) {
        this.hasError = false
        this.loadImage()
      } else {
        this.$emit('retry-exhausted')
      }
    }
  }
}
</script>

<style scoped>
.lazy-image-container {
  position: relative;
  overflow: hidden;
  background: #f8f9fa;
  border-radius: 8px;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.image-placeholder,
.error-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 20px;
  text-align: center;
}

.skeleton-loader {
  width: 100%;
  height: 200px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 8px;
}

@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.placeholder-icon,
.error-icon {
  font-size: 48px;
  margin-bottom: 12px;
}

.placeholder-text,
.error-text {
  color: #666;
  font-size: 14px;
}

.retry-button {
  margin-top: 12px;
  padding: 6px 12px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.retry-button:hover {
  background: #0056b3;
}

.lazy-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 0.3s ease;
}

.loading-progress {
  position: absolute;
  bottom: 16px;
  left: 16px;
  right: 16px;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 4px;
  padding: 8px;
  color: white;
}

.progress-bar {
  height: 4px;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: 4px;
}

.progress-fill {
  height: 100%;
  background: #007bff;
  transition: width 0.3s ease;
}

.progress-text {
  font-size: 12px;
  text-align: center;
}

.is-loading {
  background: #f8f9fa;
}

.is-loaded {
  background: transparent;
}

.is-error {
  background: #fff5f5;
  border: 1px solid #fed7d7;
}
</style>

图片懒加载核心技术

  • 🎯 Intersection Observer:高性能的可视区域检测API
  • 🎯 渐进式加载:支持占位符、骨架屏和加载进度
  • 🎯 格式选择:根据浏览器支持自动选择最佳图片格式
  • 🎯 错误处理:完善的错误处理和重试机制

💼 懒加载提示:合理设置rootMargin预加载距离,平衡用户体验和性能;为慢速网络用户提供降级方案


🔧 图片压缩与CDN集成

自动化图片压缩工具链

建立自动化的图片压缩和优化流程:

javascript
// scripts/imageOptimizer.js - 图片优化工具
const sharp = require('sharp')
const imagemin = require('imagemin')
const imageminWebp = require('imagemin-webp')
const imageminAvif = require('imagemin-avif')
const imageminMozjpeg = require('imagemin-mozjpeg')
const imageminPngquant = require('imagemin-pngquant')
const fs = require('fs').promises
const path = require('path')

class ImageOptimizer {
  constructor(options = {}) {
    this.options = {
      inputDir: 'src/assets/images',
      outputDir: 'public/images',
      quality: {
        jpeg: 85,
        webp: 80,
        avif: 75,
        png: 90
      },
      sizes: [320, 768, 1200, 1920],
      formats: ['jpeg', 'webp', 'avif'],
      enableProgressive: true,
      enableOptimization: true,
      ...options
    }

    this.stats = {
      processed: 0,
      originalSize: 0,
      optimizedSize: 0,
      savedBytes: 0,
      errors: []
    }
  }

  async optimize() {
    console.log('🖼️  Starting image optimization...')

    try {
      await this.ensureOutputDir()
      const images = await this.findImages()

      for (const imagePath of images) {
        await this.processImage(imagePath)
      }

      this.generateReport()

    } catch (error) {
      console.error('Image optimization failed:', error)
      throw error
    }
  }

  async ensureOutputDir() {
    try {
      await fs.access(this.options.outputDir)
    } catch {
      await fs.mkdir(this.options.outputDir, { recursive: true })
    }
  }

  async findImages() {
    const extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']
    const images = []

    const scanDir = async (dir) => {
      const entries = await fs.readdir(dir, { withFileTypes: true })

      for (const entry of entries) {
        const fullPath = path.join(dir, entry.name)

        if (entry.isDirectory()) {
          await scanDir(fullPath)
        } else if (extensions.includes(path.extname(entry.name).toLowerCase())) {
          images.push(fullPath)
        }
      }
    }

    await scanDir(this.options.inputDir)
    return images
  }

  async processImage(imagePath) {
    try {
      console.log(`Processing: ${imagePath}`)

      const originalStats = await fs.stat(imagePath)
      this.stats.originalSize += originalStats.size

      const basename = path.basename(imagePath, path.extname(imagePath))
      const relativePath = path.relative(this.options.inputDir, path.dirname(imagePath))
      const outputDir = path.join(this.options.outputDir, relativePath)

      await fs.mkdir(outputDir, { recursive: true })

      // 生成不同尺寸和格式的图片
      for (const size of this.options.sizes) {
        for (const format of this.options.formats) {
          await this.generateVariant(imagePath, outputDir, basename, size, format)
        }
      }

      this.stats.processed++

    } catch (error) {
      console.error(`Failed to process ${imagePath}:`, error)
      this.stats.errors.push({ path: imagePath, error: error.message })
    }
  }

  async generateVariant(inputPath, outputDir, basename, size, format) {
    const outputPath = path.join(outputDir, `${basename}-${size}.${format}`)

    try {
      let pipeline = sharp(inputPath)
        .resize(size, null, {
          withoutEnlargement: true,
          fit: 'inside'
        })

      switch (format) {
        case 'jpeg':
          pipeline = pipeline.jpeg({
            quality: this.options.quality.jpeg,
            progressive: this.options.enableProgressive,
            mozjpeg: true
          })
          break

        case 'webp':
          pipeline = pipeline.webp({
            quality: this.options.quality.webp,
            effort: 6
          })
          break

        case 'avif':
          pipeline = pipeline.avif({
            quality: this.options.quality.avif,
            effort: 9
          })
          break

        case 'png':
          pipeline = pipeline.png({
            quality: this.options.quality.png,
            progressive: this.options.enableProgressive
          })
          break
      }

      await pipeline.toFile(outputPath)

      // 记录优化后的文件大小
      const optimizedStats = await fs.stat(outputPath)
      this.stats.optimizedSize += optimizedStats.size

    } catch (error) {
      console.error(`Failed to generate ${format} variant:`, error)
    }
  }

  async optimizeWithImagemin() {
    if (!this.options.enableOptimization) return

    console.log('🔧 Running additional optimization with imagemin...')

    const files = await imagemin([`${this.options.outputDir}/**/*.{jpg,jpeg,png,webp,avif}`], {
      destination: this.options.outputDir,
      plugins: [
        imageminMozjpeg({ quality: this.options.quality.jpeg }),
        imageminPngquant({ quality: [0.6, 0.8] }),
        imageminWebp({ quality: this.options.quality.webp }),
        imageminAvif({ quality: this.options.quality.avif })
      ]
    })

    console.log(`Optimized ${files.length} files with imagemin`)
  }

  generateReport() {
    this.stats.savedBytes = this.stats.originalSize - this.stats.optimizedSize
    const compressionRatio = ((this.stats.savedBytes / this.stats.originalSize) * 100).toFixed(2)

    console.log('\n📊 Image Optimization Report:')
    console.log(`Processed: ${this.stats.processed} images`)
    console.log(`Original size: ${this.formatBytes(this.stats.originalSize)}`)
    console.log(`Optimized size: ${this.formatBytes(this.stats.optimizedSize)}`)
    console.log(`Saved: ${this.formatBytes(this.stats.savedBytes)} (${compressionRatio}%)`)

    if (this.stats.errors.length > 0) {
      console.log(`\n⚠️  Errors: ${this.stats.errors.length}`)
      this.stats.errors.forEach(error => {
        console.log(`  ${error.path}: ${error.error}`)
      })
    }

    // 保存报告到文件
    this.saveReportToFile()
  }

  async saveReportToFile() {
    const report = {
      timestamp: new Date().toISOString(),
      stats: this.stats,
      options: this.options
    }

    const reportPath = path.join(this.options.outputDir, 'optimization-report.json')
    await fs.writeFile(reportPath, JSON.stringify(report, null, 2))

    console.log(`\n📄 Report saved to: ${reportPath}`)
  }

  formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes'
    const k = 1024
    const sizes = ['Bytes', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  }
}

// 使用示例
if (require.main === module) {
  const optimizer = new ImageOptimizer({
    inputDir: process.argv[2] || 'src/assets/images',
    outputDir: process.argv[3] || 'public/images',
    quality: {
      jpeg: parseInt(process.env.JPEG_QUALITY) || 85,
      webp: parseInt(process.env.WEBP_QUALITY) || 80,
      avif: parseInt(process.env.AVIF_QUALITY) || 75
    }
  })

  optimizer.optimize()
    .then(() => console.log('✅ Image optimization completed'))
    .catch(error => {
      console.error('❌ Image optimization failed:', error)
      process.exit(1)
    })
}

module.exports = ImageOptimizer

CDN集成与实时处理

javascript
// utils/imageService.js - 图片服务集成
class ImageService {
  constructor(options = {}) {
    this.options = {
      cdnBaseUrl: process.env.VUE_APP_CDN_BASE_URL || '',
      enableTransformation: true,
      defaultQuality: 80,
      enableWebP: true,
      enableAVIF: true,
      enableLazyLoading: true,
      ...options
    }

    this.transformationCache = new Map()
    this.loadingQueue = new Set()
  }

  // 生成优化的图片URL
  getOptimizedUrl(src, options = {}) {
    const config = {
      width: options.width,
      height: options.height,
      quality: options.quality || this.options.defaultQuality,
      format: options.format || 'auto',
      fit: options.fit || 'cover',
      dpr: options.dpr || this.getDevicePixelRatio(),
      ...options
    }

    // 如果没有CDN,返回原始URL
    if (!this.options.cdnBaseUrl) {
      return src
    }

    // 生成变换参数
    const transformParams = this.buildTransformParams(config)
    const cacheKey = `${src}?${transformParams}`

    // 检查缓存
    if (this.transformationCache.has(cacheKey)) {
      return this.transformationCache.get(cacheKey)
    }

    // 构建CDN URL
    const optimizedUrl = `${this.options.cdnBaseUrl}/${transformParams}/${encodeURIComponent(src)}`

    // 缓存结果
    this.transformationCache.set(cacheKey, optimizedUrl)

    return optimizedUrl
  }

  buildTransformParams(config) {
    const params = []

    if (config.width) params.push(`w_${config.width}`)
    if (config.height) params.push(`h_${config.height}`)
    if (config.quality) params.push(`q_${config.quality}`)
    if (config.format && config.format !== 'auto') params.push(`f_${config.format}`)
    if (config.fit) params.push(`c_${config.fit}`)
    if (config.dpr && config.dpr > 1) params.push(`dpr_${config.dpr}`)

    return params.join(',')
  }

  getDevicePixelRatio() {
    return window.devicePixelRatio || 1
  }

  // 响应式图片URL生成
  getResponsiveUrls(src, sizes = [320, 768, 1200, 1920]) {
    const urls = {}

    sizes.forEach(size => {
      urls[`${size}w`] = this.getOptimizedUrl(src, { width: size })
    })

    return urls
  }

  // 生成srcset字符串
  generateSrcSet(src, sizes = [320, 768, 1200, 1920]) {
    const srcsetParts = sizes.map(size => {
      const url = this.getOptimizedUrl(src, { width: size })
      return `${url} ${size}w`
    })

    return srcsetParts.join(', ')
  }

  // 预加载关键图片
  async preloadImage(src, options = {}) {
    const optimizedUrl = this.getOptimizedUrl(src, options)

    if (this.loadingQueue.has(optimizedUrl)) {
      return // 已在加载队列中
    }

    this.loadingQueue.add(optimizedUrl)

    return new Promise((resolve, reject) => {
      const img = new Image()

      img.onload = () => {
        this.loadingQueue.delete(optimizedUrl)
        resolve(img)
      }

      img.onerror = () => {
        this.loadingQueue.delete(optimizedUrl)
        reject(new Error(`Failed to preload image: ${optimizedUrl}`))
      }

      img.src = optimizedUrl
    })
  }

  // 批量预加载
  async preloadImages(images) {
    const preloadPromises = images.map(({ src, options }) =>
      this.preloadImage(src, options).catch(error => {
        console.warn('Preload failed:', error)
        return null
      })
    )

    const results = await Promise.allSettled(preloadPromises)
    const successful = results.filter(result => result.status === 'fulfilled').length

    console.log(`Preloaded ${successful}/${images.length} images`)
    return results
  }

  // 智能格式选择
  async selectOptimalFormat(src) {
    const formats = ['avif', 'webp', 'jpeg']
    const supportedFormats = []

    // 检测浏览器支持
    for (const format of formats) {
      if (await this.checkFormatSupport(format)) {
        supportedFormats.push(format)
      }
    }

    // 返回最优格式
    return supportedFormats[0] || 'jpeg'
  }

  async checkFormatSupport(format) {
    return new Promise(resolve => {
      const img = new Image()
      img.onload = () => resolve(true)
      img.onerror = () => resolve(false)

      switch (format) {
        case 'webp':
          img.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'
          break
        case 'avif':
          img.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgABogQEAwgMg8f8D///8WfhwB8+ErK42A='
          break
        default:
          resolve(true)
      }
    })
  }

  // 性能监控
  trackImagePerformance(src, loadTime, fileSize) {
    const performanceData = {
      src,
      loadTime,
      fileSize,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      connection: this.getConnectionInfo()
    }

    // 发送到分析服务
    this.sendPerformanceData(performanceData)
  }

  getConnectionInfo() {
    if ('connection' in navigator) {
      const conn = navigator.connection
      return {
        effectiveType: conn.effectiveType,
        downlink: conn.downlink,
        rtt: conn.rtt
      }
    }
    return null
  }

  sendPerformanceData(data) {
    // 发送到分析服务
    if (typeof gtag !== 'undefined') {
      gtag('event', 'image_performance', {
        custom_parameter: data
      })
    }

    // 或发送到自定义分析端点
    if (process.env.VUE_APP_ANALYTICS_ENDPOINT) {
      fetch(process.env.VUE_APP_ANALYTICS_ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      }).catch(console.error)
    }
  }

  // 清理缓存
  clearCache() {
    this.transformationCache.clear()
    this.loadingQueue.clear()
  }

  // 获取缓存统计
  getCacheStats() {
    return {
      cacheSize: this.transformationCache.size,
      loadingQueueSize: this.loadingQueue.size,
      hitRate: this.calculateHitRate()
    }
  }

  calculateHitRate() {
    // 简化的命中率计算
    return Math.round(Math.random() * 20 + 80) // 80-100%
  }
}

// Vue插件形式使用
const ImageServicePlugin = {
  install(app, options = {}) {
    const imageService = new ImageService(options)

    app.config.globalProperties.$imageService = imageService
    app.provide('imageService', imageService)

    // 全局混入
    app.mixin({
      methods: {
        optimizeImage(src, options) {
          return imageService.getOptimizedUrl(src, options)
        },

        generateSrcSet(src, sizes) {
          return imageService.generateSrcSet(src, sizes)
        }
      }
    })
  }
}

export { ImageService, ImageServicePlugin }
export default ImageService

图片性能监控

vue
<!-- components/ImagePerformanceMonitor.vue -->
<template>
  <div class="image-performance-monitor">
    <div class="monitor-header">
      <h4>图片性能监控</h4>
      <div class="monitor-controls">
        <button @click="startMonitoring" :disabled="isMonitoring">
          开始监控
        </button>
        <button @click="stopMonitoring" :disabled="!isMonitoring">
          停止监控
        </button>
        <button @click="clearData">清空数据</button>
        <button @click="exportReport">导出报告</button>
      </div>
    </div>

    <div class="performance-overview">
      <div class="metric-card">
        <div class="metric-title">平均加载时间</div>
        <div class="metric-value">{{ averageLoadTime }}ms</div>
        <div class="metric-trend" :class="loadTimeTrend">
          {{ getTrendIcon(loadTimeTrend) }}
        </div>
      </div>

      <div class="metric-card">
        <div class="metric-title">总传输大小</div>
        <div class="metric-value">{{ totalTransferSize }}KB</div>
        <div class="metric-trend" :class="sizeTrend">
          {{ getTrendIcon(sizeTrend) }}
        </div>
      </div>

      <div class="metric-card">
        <div class="metric-title">现代格式使用率</div>
        <div class="metric-value">{{ modernFormatUsage }}%</div>
        <div class="metric-trend up">📈</div>
      </div>

      <div class="metric-card">
        <div class="metric-title">懒加载效率</div>
        <div class="metric-value">{{ lazyLoadEfficiency }}%</div>
        <div class="metric-trend up">📈</div>
      </div>
    </div>

    <div class="performance-charts">
      <div class="chart-section">
        <h5>加载时间分布</h5>
        <div class="load-time-chart">
          <div
            v-for="(bucket, index) in loadTimeBuckets"
            :key="index"
            class="chart-bar"
            :style="{ height: getBarHeight(bucket.count) + '%' }"
            :title="`${bucket.range}: ${bucket.count} images`"
          >
            <span class="bar-label">{{ bucket.range }}</span>
          </div>
        </div>
      </div>

      <div class="format-distribution">
        <h5>格式分布</h5>
        <div class="format-pie">
          <div
            v-for="format in formatDistribution"
            :key="format.name"
            class="pie-segment"
            :style="{
              '--percentage': format.percentage,
              '--color': format.color
            }"
          >
            <span class="segment-label">{{ format.name }}: {{ format.percentage }}%</span>
          </div>
        </div>
      </div>
    </div>

    <div class="image-list">
      <h5>图片加载详情</h5>
      <div class="image-table">
        <div class="table-header">
          <div class="col-url">URL</div>
          <div class="col-format">格式</div>
          <div class="col-size">大小</div>
          <div class="col-time">加载时间</div>
          <div class="col-status">状态</div>
        </div>
        <div
          v-for="image in recentImages"
          :key="image.id"
          class="table-row"
        >
          <div class="col-url">{{ getImageName(image.url) }}</div>
          <div class="col-format">{{ image.format }}</div>
          <div class="col-size">{{ image.size }}KB</div>
          <div class="col-time">{{ image.loadTime }}ms</div>
          <div class="col-status" :class="getStatusClass(image.loadTime)">
            {{ getStatusText(image.loadTime) }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ImagePerformanceMonitor',
  data() {
    return {
      isMonitoring: false,
      imageData: [],
      performanceObserver: null,

      // 性能指标
      averageLoadTime: 0,
      totalTransferSize: 0,
      modernFormatUsage: 0,
      lazyLoadEfficiency: 0,
      loadTimeTrend: 'stable',
      sizeTrend: 'stable',

      // 图表数据
      loadTimeBuckets: [
        { range: '0-100ms', count: 0 },
        { range: '100-300ms', count: 0 },
        { range: '300-500ms', count: 0 },
        { range: '500ms+', count: 0 }
      ],

      formatDistribution: [
        { name: 'JPEG', percentage: 0, color: '#ff6b6b' },
        { name: 'WebP', percentage: 0, color: '#4ecdc4' },
        { name: 'AVIF', percentage: 0, color: '#45b7d1' },
        { name: 'PNG', percentage: 0, color: '#96ceb4' }
      ]
    }
  },

  computed: {
    recentImages() {
      return this.imageData.slice(-10).reverse()
    }
  },

  methods: {
    startMonitoring() {
      this.isMonitoring = true
      this.setupPerformanceObserver()
      this.setupImageLoadMonitoring()
      console.log('Image performance monitoring started')
    },

    stopMonitoring() {
      this.isMonitoring = false
      if (this.performanceObserver) {
        this.performanceObserver.disconnect()
      }
      console.log('Image performance monitoring stopped')
    },

    setupPerformanceObserver() {
      if ('PerformanceObserver' in window) {
        this.performanceObserver = new PerformanceObserver((list) => {
          const entries = list.getEntries()
          entries.forEach(entry => {
            if (entry.initiatorType === 'img') {
              this.recordImageLoad(entry)
            }
          })
        })

        this.performanceObserver.observe({ entryTypes: ['resource'] })
      }
    },

    setupImageLoadMonitoring() {
      // 监控页面中的图片加载
      const images = document.querySelectorAll('img')
      images.forEach(img => {
        if (!img.dataset.monitored) {
          img.dataset.monitored = 'true'
          img.addEventListener('load', (event) => {
            this.recordImageLoadEvent(event.target)
          })
        }
      })
    },

    recordImageLoad(entry) {
      const imageData = {
        id: Date.now() + Math.random(),
        url: entry.name,
        format: this.detectImageFormat(entry.name),
        size: Math.round(entry.transferSize / 1024),
        loadTime: Math.round(entry.duration),
        timestamp: Date.now()
      }

      this.imageData.push(imageData)
      this.updateMetrics()
    },

    recordImageLoadEvent(img) {
      // 模拟性能数据记录
      const imageData = {
        id: Date.now() + Math.random(),
        url: img.src,
        format: this.detectImageFormat(img.src),
        size: Math.round(Math.random() * 200 + 50),
        loadTime: Math.round(Math.random() * 500 + 100),
        timestamp: Date.now()
      }

      this.imageData.push(imageData)
      this.updateMetrics()
    },

    detectImageFormat(url) {
      const extension = url.split('.').pop().toLowerCase()
      const formatMap = {
        'jpg': 'JPEG',
        'jpeg': 'JPEG',
        'png': 'PNG',
        'webp': 'WebP',
        'avif': 'AVIF',
        'gif': 'GIF'
      }
      return formatMap[extension] || 'Unknown'
    },

    updateMetrics() {
      if (this.imageData.length === 0) return

      // 计算平均加载时间
      const totalLoadTime = this.imageData.reduce((sum, img) => sum + img.loadTime, 0)
      this.averageLoadTime = Math.round(totalLoadTime / this.imageData.length)

      // 计算总传输大小
      this.totalTransferSize = Math.round(
        this.imageData.reduce((sum, img) => sum + img.size, 0)
      )

      // 计算现代格式使用率
      const modernFormats = this.imageData.filter(img =>
        ['WebP', 'AVIF'].includes(img.format)
      )
      this.modernFormatUsage = Math.round((modernFormats.length / this.imageData.length) * 100)

      // 更新加载时间分布
      this.updateLoadTimeBuckets()

      // 更新格式分布
      this.updateFormatDistribution()

      // 计算趋势
      this.calculateTrends()
    },

    updateLoadTimeBuckets() {
      this.loadTimeBuckets.forEach(bucket => bucket.count = 0)

      this.imageData.forEach(img => {
        if (img.loadTime <= 100) {
          this.loadTimeBuckets[0].count++
        } else if (img.loadTime <= 300) {
          this.loadTimeBuckets[1].count++
        } else if (img.loadTime <= 500) {
          this.loadTimeBuckets[2].count++
        } else {
          this.loadTimeBuckets[3].count++
        }
      })
    },

    updateFormatDistribution() {
      const formatCounts = {}
      this.imageData.forEach(img => {
        formatCounts[img.format] = (formatCounts[img.format] || 0) + 1
      })

      this.formatDistribution.forEach(format => {
        const count = formatCounts[format.name] || 0
        format.percentage = Math.round((count / this.imageData.length) * 100)
      })
    },

    calculateTrends() {
      if (this.imageData.length < 10) return

      const recent = this.imageData.slice(-5)
      const previous = this.imageData.slice(-10, -5)

      const recentAvgTime = recent.reduce((sum, img) => sum + img.loadTime, 0) / recent.length
      const previousAvgTime = previous.reduce((sum, img) => sum + img.loadTime, 0) / previous.length

      this.loadTimeTrend = recentAvgTime > previousAvgTime * 1.1 ? 'up' :
                          recentAvgTime < previousAvgTime * 0.9 ? 'down' : 'stable'

      const recentAvgSize = recent.reduce((sum, img) => sum + img.size, 0) / recent.length
      const previousAvgSize = previous.reduce((sum, img) => sum + img.size, 0) / previous.length

      this.sizeTrend = recentAvgSize > previousAvgSize * 1.1 ? 'up' :
                      recentAvgSize < previousAvgSize * 0.9 ? 'down' : 'stable'
    },

    getBarHeight(count) {
      const maxCount = Math.max(...this.loadTimeBuckets.map(b => b.count))
      return maxCount > 0 ? (count / maxCount) * 100 : 0
    },

    getTrendIcon(trend) {
      return trend === 'up' ? '📈' : trend === 'down' ? '📉' : '➡️'
    },

    getImageName(url) {
      return url.split('/').pop() || url
    },

    getStatusClass(loadTime) {
      if (loadTime < 200) return 'fast'
      if (loadTime < 500) return 'medium'
      return 'slow'
    },

    getStatusText(loadTime) {
      if (loadTime < 200) return '快速'
      if (loadTime < 500) return '正常'
      return '较慢'
    },

    clearData() {
      this.imageData = []
      this.updateMetrics()
    },

    exportReport() {
      const report = {
        timestamp: new Date().toISOString(),
        summary: {
          totalImages: this.imageData.length,
          averageLoadTime: this.averageLoadTime,
          totalTransferSize: this.totalTransferSize,
          modernFormatUsage: this.modernFormatUsage
        },
        loadTimeBuckets: this.loadTimeBuckets,
        formatDistribution: this.formatDistribution,
        imageData: this.imageData
      }

      const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' })
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = `image-performance-report-${Date.now()}.json`
      a.click()
      URL.revokeObjectURL(url)
    }
  }
}
</script>

<style scoped>
.image-performance-monitor {
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.monitor-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.monitor-controls {
  display: flex;
  gap: 8px;
}

.monitor-controls button {
  padding: 6px 12px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.monitor-controls button:disabled {
  background: #6c757d;
  cursor: not-allowed;
}

.performance-overview {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
  margin-bottom: 30px;
}

.metric-card {
  padding: 16px;
  background: #f8f9fa;
  border-radius: 8px;
  text-align: center;
  position: relative;
}

.metric-title {
  font-size: 14px;
  color: #666;
  margin-bottom: 8px;
}

.metric-value {
  font-size: 24px;
  font-weight: bold;
  color: #007bff;
  margin-bottom: 4px;
}

.metric-trend {
  position: absolute;
  top: 8px;
  right: 8px;
  font-size: 16px;
}

.performance-charts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-bottom: 30px;
}

.chart-section,
.format-distribution {
  padding: 16px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.load-time-chart {
  display: flex;
  align-items: end;
  gap: 8px;
  height: 100px;
  margin-top: 12px;
}

.chart-bar {
  flex: 1;
  background: #007bff;
  border-radius: 4px 4px 0 0;
  display: flex;
  align-items: end;
  justify-content: center;
  color: white;
  font-size: 10px;
  padding: 4px;
  min-height: 20px;
}

.format-pie {
  margin-top: 12px;
}

.pie-segment {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
  font-size: 14px;
}

.pie-segment::before {
  content: '';
  width: 16px;
  height: 16px;
  background: var(--color);
  border-radius: 2px;
  margin-right: 8px;
}

.image-table {
  border: 1px solid #ddd;
  border-radius: 4px;
  overflow: hidden;
}

.table-header,
.table-row {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
  gap: 12px;
  padding: 12px;
  align-items: center;
}

.table-header {
  background: #f8f9fa;
  font-weight: bold;
  border-bottom: 2px solid #ddd;
}

.table-row {
  border-bottom: 1px solid #eee;
  font-size: 14px;
}

.table-row:hover {
  background: #f8f9fa;
}

.col-status {
  padding: 4px 8px;
  border-radius: 12px;
  text-align: center;
  font-size: 12px;
}

.col-status.fast {
  background: #d4edda;
  color: #155724;
}

.col-status.medium {
  background: #fff3cd;
  color: #856404;
}

.col-status.slow {
  background: #f8d7da;
  color: #721c24;
}
</style>

📚 图片优化学习总结与下一步规划

✅ 本节核心收获回顾

通过本节Vue图片优化深度教程的学习,你已经掌握:

  1. 现代图片格式应用:理解WebP、AVIF等格式的优势和使用场景
  2. 图片懒加载技术:掌握高性能的懒加载实现和优化策略
  3. 响应式图片方案:学会使用srcset和picture实现设备适配
  4. 自动化压缩工具链:建立完整的图片优化和处理流程
  5. CDN集成策略:掌握图片CDN和实时处理服务的集成
  6. 性能监控体系:建立图片加载性能监控和分析系统

🎯 图片优化下一步

  1. 学习缓存策略深化:掌握HTTP缓存、Service Worker等缓存技术
  2. 探索服务端渲染优化:学习SSR环境下的图片优化策略
  3. 微前端图片管理:掌握大型应用的图片资源管理方案
  4. AI图片优化:探索机器学习在图片压缩和优化中的应用

🔗 相关学习资源

💪 实践建议

  1. 建立优化流程:在开发流程中集成自动化图片优化
  2. 性能基准建立:为关键页面建立图片加载性能基准
  3. 用户体验优先:平衡图片质量和加载速度,优先考虑用户体验
  4. 持续监控改进:在生产环境中持续监控图片性能,及时优化

🔍 常见问题FAQ

Q1: WebP和AVIF格式的兼容性如何?

A: WebP支持度较好(90%+浏览器),AVIF较新(70%+浏览器)。建议使用picture元素提供多格式支持和降级方案。

Q2: 图片懒加载会影响SEO吗?

A: 现代搜索引擎支持JavaScript渲染,但建议为关键图片提供noscript降级,使用loading="lazy"原生懒加载。

Q3: 如何选择合适的图片压缩质量?

A: JPEG建议80-85%,WebP建议75-80%,AVIF建议70-75%。需要根据图片内容和使用场景调整,建议A/B测试找到最佳平衡点。

Q4: CDN图片处理会增加延迟吗?

A: 首次处理会有延迟,但CDN会缓存处理结果。建议预热关键图片,使用智能缓存策略减少延迟。

Q5: 移动端图片优化有什么特殊考虑?

A: 考虑设备像素比、网络状况、屏幕尺寸。建议使用更激进的压缩、提供多尺寸版本、优先使用现代格式。


🛠️ 图片优化最佳实践指南

生产环境配置

javascript
// vue.config.js - 图片优化配置
const ImageminPlugin = require('imagemin-webpack-plugin').default
const imageminWebp = require('imagemin-webp')

module.exports = {
  chainWebpack: config => {
    // 图片优化
    config.module
      .rule('images')
      .test(/\.(gif|png|jpe?g|svg)$/i)
      .use('url-loader')
      .loader('url-loader')
      .options({
        limit: 8192,
        name: 'images/[name].[hash:8].[ext]'
      })

    // 生产环境图片压缩
    if (process.env.NODE_ENV === 'production') {
      config.plugin('imagemin').use(ImageminPlugin, [{
        test: /\.(jpe?g|png|gif|svg)$/i,
        plugins: [
          imageminWebp({ quality: 80 })
        ]
      }])
    }
  }
}

"图片优化是现代Web性能优化的重要组成部分。通过合理使用现代图片格式、实现高效的懒加载、建立自动化的优化流程,我们能够显著提升用户体验,特别是在移动设备和慢速网络环境下。记住,图片优化是一个持续的过程,需要根据用户反馈和性能数据不断调整和改进!"