问题场景
在开发一个需求时,页面分为两个阶段:
-
第一阶段:播放一段动画视频(占满全屏,约3-5秒)
-
第二阶段:展示一个列表,列表中的子项包含多张背景图和图标
问题:切换到第二阶段时,图片还在加载,用户看到的是不完整的界面。
最简解决方案
经过思考,最简单有效的方案就是:在第一阶段用 new Image() 预加载第二阶段的图片。
核心代码
// 预加载单张图片
const preloadImage = (src) => {
return new Promise((resolve) => {
if (!src) {
resolve(false)
return
}
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
img.src = src // 🔑 关键就是这一行!
})
}
// 批量预加载
const preloadImages = (srcList) => {
const promises = srcList.map(src => preloadImage(src))
return Promise.all(promises)
}
// 在第一阶段立即开始预加载
const initAnimation = async () => {
// 获取数据...
memberLevelId.value = levelId
benefits.value = benefitsList
// 🚀 立即预加载第二阶段图片
startPreloadImages()
}
效果
-
第一阶段:视频播放的同时,静默预加载图片
-
第二阶段:图片从浏览器缓存瞬间加载,零网络延迟
技术原理深度解析
1. 浏览器缓存机制
当执行 img.src = url 时,浏览器会:
1. 检查内存缓存 (Memory Cache)
2. 检查磁盘缓存 (Disk Cache)
3. 如果缓存未命中,发起网络请求
4. 下载完成后,存储到缓存中
关键点:即使 Image 对象没有被添加到DOM,浏览器仍然会缓存图片资源。
2. 缓存策略分析
// 示例:观察缓存效果
const testCache = async () => {
console.time('首次加载')
await preloadImage('https://example.com/large-image.jpg')
console.timeEnd('首次加载') // 可能耗时 200-500ms
console.time('缓存加载')
const img = document.createElement('img')
img.src = 'https://example.com/large-image.jpg'
document.body.appendChild(img)
console.timeEnd('缓存加载') // 通常 < 5ms
}
3. HTTP 缓存头的影响
不同的缓存头会影响预加载效果:
# 强缓存(最佳)
Cache-Control: max-age=31536000
Expires: Thu, 31 Dec 2024 23:59:59 GMT
# 协商缓存
Cache-Control: no-cache
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
# 禁用缓存(预加载无效)
Cache-Control: no-store
不同实现方案对比
方案一:Image 对象预加载
const preloadWithImage = (src) => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
img.src = src
})
}
优点:
- ✅ 简单易用,兼容性好
- ✅ 支持进度监控
- ✅ 可以获取图片尺寸信息
缺点:
- ❌ 不支持跨域图片的某些操作
- ❌ 无法直接控制优先级
最终选择他,因为
- 微信/H5全兼容
- 代码量最小(核心仅3行)
- 展示项目中验证:预加载20+图片零失败
方案二:Link 标签预加载(不推荐)
const preloadWithLink = (src) => {
return new Promise((resolve) => {
const link = document.createElement('link')
link.rel = 'preload'
link.as = 'image'
link.href = src
link.onload = () => resolve(true)
link.onerror = () => resolve(false)
document.head.appendChild(link)
})
}
优点:
-
✅ 浏览器高优先级处理
-
✅ 支持 CORS 设置
-
✅ 更好的性能表现
缺点:
- ❌ 需要手动清理 DOM
- ❌ 兼容性差
方案三:Service Worker 预加载(不推荐)
// service-worker.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('images-v1').then((cache) => {
return cache.addAll([
'/images/bg1.jpg',
'/images/bg2.jpg',
'/images/icon1.png'
])
})
)
})
// main.js
const preloadWithSW = async (urls) => {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('/sw.js')
// Service Worker 会自动缓存指定资源
}
}
优点:
-
✅ 离线可用
-
✅ 精确的缓存控制
-
✅ 跨页面共享缓存
缺点:
- ❌ 实现复杂
- ❌ 调试困难
- ❌ 兼容性要求高
最佳实践指南
1. 预加载时机选择
// ✅ 好的时机
路由切换前
用户交互触发前
空闲时间 (requestIdleCallback)
关键资源加载完成后
// ❌ 避免的时机
页面初始化时(影响首屏)
用户正在交互时
网络状况较差时
2. 优先级管理
const preloadManager = {
highPriority: [], // 关键图片
lowPriority: [], // 次要图片
async preloadByPriority() {
// 优先加载关键图片
await this.preloadImages(this.highPriority)
// 在空闲时间加载次要图片
requestIdleCallback(() => {
this.preloadImages(this.lowPriority)
})
}
}
3. 错误处理与降级
const robustPreload = async (src, retryCount = 3) => {
for (let i = 0; i < retryCount; i++) {
try {
const success = await preloadImage(src)
if (success) return true
} catch (error) {
console.warn(预加载失败,重试 ${i + 1}/${retryCount}:, src)
// 延迟重试
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
}
// 最终失败,但不阻断流程
console.error('预加载最终失败:', src)
return false
}
开发工具与调试
1. 预加载效果监控
class PreloadMonitor {
constructor() {
this.stats = {
total: 0,
success: 0,
failed: 0,
totalTime: 0
}
}
async monitorPreload(src) {
this.stats.total++
const startTime = performance.now()
try {
const success = await preloadImage(src)
const endTime = performance.now()
this.stats.totalTime += (endTime - startTime)
if (success) {
this.stats.success++
} else {
this.stats.failed++
}
this.logStats()
return success
} catch (error) {
this.stats.failed++
this.logStats()
return false
}
}
logStats() {
console.log('预加载统计:', {
成功率: `${(this.stats.success / this.stats.total * 100).toFixed(2)}%`,
平均耗时: `${(this.stats.totalTime / this.stats.total).toFixed(2)}ms`,
总计: this.stats.total
})
}
}
2. Chrome DevTools 调试技巧
// 在控制台查看缓存状态
const checkCacheStatus = (url) => {
const img = new Image()
img.src = url
img.onload = () => {
console.log(✅ ${url} 已缓存)
}
img.onerror = () => {
console.log(❌ ${url} 缓存失效)
}
}
// 批量检查
['image1.jpg', 'image2.jpg'].forEach(checkCacheStatus)
总结
图片预加载看似简单,但要做好需要考虑很多因素:
核心要点
-
简单场景:直接使用
new Image()就能解决大部分问题 -
性能优化:选择合适的图片格式和尺寸
-
用户体验:在不影响主要功能的前提下进行预加载
技术选择
其实绝大多数场景直接使用Image 对象预加载即可,实现简单收益高,毕竟只是个预加载..
标签: 前端性能 图片优化 用户体验 JavaScript 缓存策略