diff --git a/playground/app/pages/placeholder-regression.vue b/playground/app/pages/placeholder-regression.vue new file mode 100644 index 000000000..786419653 --- /dev/null +++ b/playground/app/pages/placeholder-regression.vue @@ -0,0 +1,10 @@ + diff --git a/playground/app/providers.ts b/playground/app/providers.ts index 0eee2db34..663604c3a 100644 --- a/playground/app/providers.ts +++ b/playground/app/providers.ts @@ -221,6 +221,7 @@ export const providers: Provider[] = [ width: 500, height: 500, fit: 'contain', + placeholder: true, }, { src: 'boat.jpg', diff --git a/src/runtime/components/NuxtImg.vue b/src/runtime/components/NuxtImg.vue index 875d1216e..d8cb8e7a0 100644 --- a/src/runtime/components/NuxtImg.vue +++ b/src/runtime/components/NuxtImg.vue @@ -22,9 +22,9 @@ import type { ImgHTMLAttributes } from 'vue' import { useImage } from '../composables' import { prerenderStaticImages } from '../utils/prerender' -import { markFeatureUsage } from '../utils/performance' import { useImageProps } from '../utils/props' import type { BaseImageProps } from '../utils/props' +import { markFeatureUsage } from '../utils/performance' import type { ProviderDefaults, ConfiguredImageProviders } from '@nuxt/image' import { useHead, useNuxtApp, useRequestEvent } from '#imports' @@ -57,15 +57,46 @@ const sizes = computed(() => $img.getSizes(props.src!, { const placeholderLoaded = ref(false) const attrs = useAttrs() as ImgHTMLAttributes -const imgAttrs = computed(() => ({ - ...normalizedAttrs.value, - 'data-nuxt-img': '', - ...(!props.placeholder || placeholderLoaded.value) - ? { sizes: sizes.value.sizes, srcset: sizes.value.srcset } - : {}, - ...import.meta.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {}, - ...attrs, -})) +const imgAttrs = computed(() => { + const base: ImgHTMLAttributes = { + ...normalizedAttrs.value, + 'data-nuxt-img': '', + ...!props.placeholder + ? { sizes: sizes.value.sizes, srcset: sizes.value.srcset } + : {}, + ...import.meta.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {}, + } + + if (props.placeholder) { + const chainHandlers = (a?: any, b?: any) => + (a && b) + ? (...args: any[]) => { + a(...args) + b(...args) + } + : (a || b) + + base.onLoad = chainHandlers(base.onLoad, (event: Event) => { + if (!placeholderLoaded.value) { + // Placeholder just loaded, trigger src update to mainSrc + placeholderLoaded.value = true + } + else { + // Main image loaded + emit('load', event) + } + }) + + base.onError = chainHandlers(base.onError, (event: Event) => { + emit('error', event) + }) + } + + return { + ...base, + ...attrs, + } +}) const placeholder = computed(() => { if (placeholderLoaded.value) { @@ -130,31 +161,7 @@ if (import.meta.server && import.meta.prerender) { const initialLoad = useNuxtApp().isHydrating const imgEl = useTemplateRef('imgEl') onMounted(() => { - if (placeholder.value || props.custom) { - const img = new Image() - - if (mainSrc.value) { - img.src = mainSrc.value - } - - if (props.sizes) { - img.sizes = sizes.value.sizes || '' - img.srcset = sizes.value.srcset - } - - img.onload = (event) => { - placeholderLoaded.value = true - emit('load', event) - } - - img.onerror = (event) => { - emit('error', event) - } - - markFeatureUsage('nuxt-image') - - return - } + markFeatureUsage('nuxt-img') if (!imgEl.value) { return @@ -164,17 +171,22 @@ onMounted(() => { if (imgEl.value.getAttribute('data-error')) { emit('error', new Event('error')) } + else if (props.placeholder && !placeholderLoaded.value) { + placeholderLoaded.value = true + } else { emit('load', new Event('load')) } } - imgEl.value.onload = (event) => { - emit('load', event) - } + if (!props.placeholder) { + imgEl.value.onload = (event) => { + emit('load', event) + } - imgEl.value.onerror = (event) => { - emit('error', event) + imgEl.value.onerror = (event) => { + emit('error', event) + } } }) diff --git a/test/e2e/ssr.test.ts b/test/e2e/ssr.test.ts index cab6c08de..d81b245fa 100644 --- a/test/e2e/ssr.test.ts +++ b/test/e2e/ssr.test.ts @@ -84,4 +84,28 @@ describe('browser (ssr: true)', () => { } `) }) + + it('should not load the main image twice when placeholder is enabled', async () => { + const page = await createPage() + + const requests: string[] = [] + await page.route('**', (route) => { + requests.push(route.request().url()) + return route.continue() + }) + + await page.goto(url('/placeholder-regression'), { waitUntil: 'networkidle' }) + + await page.waitForSelector('img') + + const mainImageRequests = requests.filter(r => + r.includes('/_ipx/') + && r.includes('images/colors.jpg') + && r.includes('s_500x500'), + ) + + expect(mainImageRequests.length).toBe(1) + + await page.close() + }) })