Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Vue图片优化教程,详解WebP、AVIF格式、懒加载、响应式图片。包含完整优化方案,适合Vue.js开发者快速掌握图片性能优化技术。
核心关键词:Vue图片优化2024、WebP格式、图片懒加载、响应式图片、Vue图片性能
长尾关键词:Vue图片懒加载怎么实现、WebP格式如何使用、响应式图片最佳实践、图片压缩优化策略、前端图片性能优化
通过本节Vue图片优化深度教程,你将系统性掌握:
为什么图片优化如此重要?这是现代Web性能优化的关键问题。图片通常占据网页总大小的60-70%,是影响加载速度的主要因素。合理的图片优化策略能够显著提升用户体验,特别是在移动设备和慢速网络环境下,也是现代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>图片懒加载是指只在图片进入可视区域时才开始加载,显著减少初始页面加载时间:
<!-- 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>图片懒加载核心技术:
💼 懒加载提示:合理设置rootMargin预加载距离,平衡用户体验和性能;为慢速网络用户提供降级方案
建立自动化的图片压缩和优化流程:
// 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// 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<!-- 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图片优化深度教程的学习,你已经掌握:
A: WebP支持度较好(90%+浏览器),AVIF较新(70%+浏览器)。建议使用picture元素提供多格式支持和降级方案。
A: 现代搜索引擎支持JavaScript渲染,但建议为关键图片提供noscript降级,使用loading="lazy"原生懒加载。
A: JPEG建议80-85%,WebP建议75-80%,AVIF建议70-75%。需要根据图片内容和使用场景调整,建议A/B测试找到最佳平衡点。
A: 首次处理会有延迟,但CDN会缓存处理结果。建议预热关键图片,使用智能缓存策略减少延迟。
A: 考虑设备像素比、网络状况、屏幕尺寸。建议使用更激进的压缩、提供多尺寸版本、优先使用现代格式。
// 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性能优化的重要组成部分。通过合理使用现代图片格式、实现高效的懒加载、建立自动化的优化流程,我们能够显著提升用户体验,特别是在移动设备和慢速网络环境下。记住,图片优化是一个持续的过程,需要根据用户反馈和性能数据不断调整和改进!"