# 图片预加载
img标签会直接下载图片
而使用new Image()则可以实现预加载
封装一个预加载组件ImageLoad,包含加载中展示和加载失败展示(支持插槽)
<template>
<slot v-if="error" name="error" class="error-placeholder">Image failed to load</slot>
<template v-else-if="showImage">
<img ref="imgRef" :src="imgSrc" />
</template>
<slot v-else name="loading" class="loading-placeholder">Image Loading...</slot>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface ImageProps {
src: string
mode?: 'lazy' | 'preload'
width?: number | string
height?: number | string
}Ï
defineOptions({ name: 'NewImage' })
const { src, mode = 'lazy', width, height } = defineProps<ImageProps>()
const error = ref<boolean>(false)
const loading = ref<boolean>(true)
const imgSrc = ref('')
const imgRef = ref<HTMLImageElement>()
const showImage = ref<boolean>(false)
const preload = () => {
const img = new Image()
img.src = src
img.onload = () => {
imgSrc.value = img.src
console.log('图片预加载完成')
showImage.value = true
}
img.onerror = () => {
error.value = true
console.error('图片加载失败')
}
}
mode === 'preload' && preload()
const retry = () => {
error.value = false
showImage.value = false
preload()
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
经过优化后的代码,支持预加载和懒加载模式
<template>
<div :style="{ width: `${width}px` }" v-if="error" @click="retry" class="error-placeholder">
<slot name="error">Image failed to load.Click to retry.</slot>
</div>
<img
v-if="mode === 'lazy'"
ref="imgRef"
:width="width"
:height="height"
:data-src="src"
v-lazy-load-img
@load="loading = false"
/>
<template v-else-if="mode === 'preload' && showImage">
<img :width="width" :height="height" ref="imgRef" :src="imgSrc" />
</template>
<slot v-if="loading" name="loading" class="loading-placeholder">Image Loading...</slot>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({ name: 'NewImage' })
const { src, mode = 'lazy', width, height } = defineProps<ImageProps>()
const error = ref<boolean>(false)
const loading = ref<boolean>(true)
const imgSrc = ref('')
const imgRef = ref<HTMLImageElement>()
const showImage = ref<boolean>(false)
const preload = () => {
const img = new Image()
img.src = src
img.onload = () => {
imgSrc.value = img.src
console.log('图片预加载完成')
showImage.value = true
}
img.onerror = () => {
error.value = true
console.error('图片加载失败')
}
}
mode === 'preload' && preload()
const retry = () => {
error.value = false
showImage.value = false
preload()
}
</script>
<style lang="css" scoped>
.loading-placeholder {
width: 100%;
height: 200px;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #999;
}
.error-placeholder {
width: 100%;
height: 200px;
background-color: #ffe5e5;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #e53935;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 图片懒加载
# 1.自定义指令
const lazyLoadImgDirective: ObjectDirective = {
mounted: (el: HTMLElement) => {
if (el.tagName === 'IMG') {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement
img.src = img.dataset.src || ''
img.dataset.src = ''
observer.unobserve(img)
}
})
observer.observe(el)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
how to use:
<img data-src="https://picsum.photos/1000/2000" v-lazy-load-img />
1
这里需要注意的是
IntersectionObserver 的触发条件是:
- 元素必须存在于 DOM 中且参与页面布局(未被
display: none隐藏)。 - 元素的位置和尺寸需要能被浏览器计算。
也就是说:如果元素被v-show 隐藏(display: none),则:
- 元素不会触发 IntersectionObserver 的回调,因为它被移出布局计算。
- Observer 无法检测到元素的可见性变化,即使后续元素变为可见(
display: block),Observer 也不会自动重新检测。
# 2.img原生loading="lazy"
img元素原生支持loading="lazy"懒加载
mdn是如此介绍的:
延迟加载图像,直到它和视口接近到一个计算得到的距离(由浏览器定义)。
# 3.使用 lazysizes 库
https://github.com/aFarkas/lazysizes
npm install lazysizes
1
使用:
给图片设置data-src属性和class="lazyload"即可
<img
data-src="large-image.jpg"
alt="A large image"
width="600"
height="400"
class="lazyload"
/>
1
2
3
4
5
6
7
2
3
4
5
6
7
# 使用decode API优化图片加载
1️⃣ 避免渲染阻塞
默认情况下,浏览器会在 img 加载后立即渲染图片,这可能导致 主线程阻塞,影响页面交互。而 image.decode() 会在 后台 解析图片数据,等图片 完全解码 后再渲染,避免卡顿。
2️⃣ 提升首屏渲染
使用 image.decode() 可以在 合适的时机 渲染图片,减少页面白屏时间,特别适用于 大图片 或 多图片加载 的场景。
# Vite项目中批量导入图片并使用
方法一:
async function loadImages(images: Record<string, () => Promise<any>>) {
const imagesModules = await Promise.all(Object.keys(images).map((key: string) => images[key]()))
return imagesModules.map((module) => module.default)
}
1
2
3
4
2
3
4
<img width="50" v-for="img in imageUrls" :key="img" :src="img" />
<script setup lang="ts">
const images = import.meta.glob(`@/assets/images/platform-icon*.png`)
const imageUrls = ref<string[]>([])
AutoloadImages(images).then((urls) => (imageUrls.value = urls))
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
方法二:
<img width="50" v-for="img in images2Urls" :key="img" :src="img" />
<script setup lang="ts">
const images2: Record<string, any> = import.meta.glob('@/assets/images/platform-icon*.png', {
eager: true,
import: 'default'
})
const images2Urls = Object.values(images2)
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
方法三:
如果img遍历的不是导入的字段,而是字符串名称
<img width="50" v-for="img in imgList" :key="img.name" :src="imageMap[img.name]" />
<script setup lang="ts">
const imgList = [
{ name: 'platform-icon1' },
{ name: 'platform-icon2' },
{ name: 'platform-icon3' },
{ name: 'platform-icon4' }
]
const images2 = import.meta.glob<string>('@/assets/images/platform-icon*.png', {
eager: true,
import: 'default'
})
const imageMap: Record<string, string> = {}
Object.entries(images2).forEach(([key, value]) => {
const filename = key.split('/').pop()?.replace('.png', '')
if (filename) {
imageMap[filename] = value as string
}
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21