# 图片预加载

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

经过优化后的代码,支持预加载和懒加载模式

<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

# 图片懒加载

# 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

how to use:

<img data-src="https://picsum.photos/1000/2000" v-lazy-load-img />
1

这里需要注意的是

IntersectionObserver 的触发条件是:

  1. 元素必须存在于 DOM 中且参与页面布局(未被 display: none 隐藏)。
  2. 元素的位置和尺寸需要能被浏览器计算。

也就是说:如果元素被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

# 使用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
<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

方法二:

<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

方法三:

如果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