Deliver images
How Nuxt Photo sizes every thumbnail, slide and photo - srcset from your Nuxt Image screens, sizes from the layout, priority and preload for the first screen, build-time blur placeholders, browser cache headers, and the Nuxt Image config for Vercel, static sites, IPX and Cloudinary.
Nuxt Photo asks your image service for the widths a photo can be shown at, and
tells the browser how wide each photo is on screen. The browser then downloads
one file close to the size it needs. With @nuxt/image installed this works
without Nuxt Photo options: quality, format, widths and the provider all come
from your image config.
How a size is chosen
Every image gets two attributes:
| Attribute | Who writes it | What it says |
|---|---|---|
srcset | Nuxt Photo, from the widths your image service allows | The files that exist: …?w=256 256w, …?w=640 640w, … |
sizes | The component that knows the layout | How wide this image is on screen: auto, 212px |
The browser multiplies the sizes width by the screen's pixel density and
picks the smallest file that is large enough. Two rules keep this correct:
- No file is wider than the source. A 1000 px photo never gets a
1280wentry. The source width is the last entry when it falls between two widths. - The layout owns
sizes.PhotoAlbumwrites the exact width of each thumbnail from its layout, already in the server-rendered HTML.PhotoCarouselusesslideSizeand the measured track. The lightbox uses the size of its frame.<Photo>usesautowith a fallback. Asizesprop onPhotoAlbumorPhotoImagereplaces the computed value.
sizes="auto, …" lets browsers that support it use the measured width of a
lazy image. Other browsers use the pixel value after it.
Use Nuxt Image
export default defineNuxtConfig({
modules: ['@nuxt/image', '@lupinum/nuxt-photo'],
image: {
quality: 80,
},
})Nuxt Photo detects @nuxt/image and uses its default provider. Module order
does not matter. These Nuxt Image options apply to every Nuxt Photo image:
| Nuxt Image option | Effect in Nuxt Photo |
|---|---|
provider | Builds every URL. Choose another one for Nuxt Photo only with nuxtPhoto.provider: 'cloudinary', or per component with provider="cloudinary". |
screens × densities | The widths in srcset. With the defaults this is 640 to 3072 px. For IPX and other providers that accept any width, Nuxt Photo adds 128, 256, 384 and 512 px below that, for thumbnails. |
quality | Sent with every image. |
format | The first format is sent to IPX. Without a format, IPX gets webp. Vercel, Netlify and Cloudinary choose AVIF or WebP from the browser's Accept header instead. |
domains | Hosts of remote images, without https://. A remote image whose host is missing fails to load. |
Animated GIFs never get a format, so they keep their animation. SVG files
are passed through unchanged, with no srcset.
Deploy on Vercel
On Vercel, Nuxt Image uses the vercel provider by itself. Vercel only
serves widths from a list that Nuxt Image builds from image.screens alone,
not from screens × densities. Nuxt Photo uses exactly that list, so no width
is rejected. The default screens run from 640 to 1536 px, so add small widths
for thumbnails and large widths for 2× screens:
export default defineNuxtConfig({
modules: ['@nuxt/image', '@lupinum/nuxt-photo'],
image: {
quality: 80,
screens: {
'3xs': 128,
'2xs': 256,
xs: 384,
'3xl': 1920,
'4xl': 2560,
'5xl': 3072,
},
},
})The default screens (sm to 2xl) stay, because Nuxt Image merges this
object with its defaults. Without the small screens, a 64 px thumbnail
downloads a 640 px file. Without the large ones, a lightbox on a 2× laptop
gets at most 1536 px.
Vercel keeps each transformed image on its CDN. For files in public/ this
lasts up to 31 days, and a redeploy does not reset it. For remote images it
lasts for the longer of the source's Cache-Control: max-age and
minimumCacheTTL, which Nuxt Image sets to 300 seconds. When your CMS gives
an edited image a new URL, raise it so visitors do not wait for a new
transform every five minutes:
export default defineNuxtConfig({
nitro: {
vercel: { config: { images: { minimumCacheTTL: 2678400 } } },
},
})Use the vercel provider for server-rendered sites on Vercel. IPX inside a
Vercel function cannot read public/, so local images return 404.
Let browsers keep images
Without a cache header, browsers ask the server again for every image on every visit. That costs a round trip per image while scrolling, even when the CDN answers from its cache. Two defaults cause it:
| Where | Default | Effect |
|---|---|---|
Vercel, files in public/ | max-age=0, must-revalidate | The optimized image inherits it. |
| IPX | image.ipx.maxAge: 60 | Browsers and CDNs keep a transformed image for one minute. |
Give image folders a long cache time:
export default defineNuxtConfig({
routeRules: {
'/photos/**': {
headers: { 'cache-control': 'public, max-age=2592000, stale-while-revalidate=604800' },
},
},
image: {
ipx: { maxAge: 2592000 },
},
})Vercel passes the source file's max-age on to the optimized image. Use 30
days: Vercel keeps an optimized image up to 31 days, and a shorter max-age
makes browsers ask again once the cached copy is older than that. When you
replace an image, give it a new file name, so browsers do not keep the old
one.
Generate a static site
With nuxi generate, Nuxt Image uses ipxStatic: every width in every
srcset is written to .output/public at build time, and no image is
transformed when a visitor scrolls. The build takes longer with many photos.
Thumbnails and slides that the server renders are included. A gallery that
renders only in the browser (inside <ClientOnly>) is not; prerender its
route or use a provider that transforms at request time.
When IPX transforms at request time on your own server, keep format at
webp. AVIF takes much longer to encode, and the first visitor waits for it.
Use Cloudinary or another provider
Any Nuxt Image provider works. Set it as the default provider, or only for Nuxt Photo:
export default defineNuxtConfig({
modules: ['@nuxt/image', '@lupinum/nuxt-photo'],
image: {
cloudinary: {
baseURL: 'https://res.cloudinary.com/<your-cloud-name>/image/upload/',
},
},
nuxtPhoto: {
provider: 'cloudinary',
},
})Set each photo's src to the asset path after baseURL, with its original
size:
const photo: PhotoItem = {
id: asset.public_id,
src: `/${asset.public_id}.${asset.format}`,
width: asset.width,
height: asset.height,
alt: asset.context?.custom?.alt,
}For an image service without a Nuxt Image provider, or in plain Vue, write a
PhotoProvider: see integrate a custom image service.
Show a placeholder while an image loads
Nuxt Photo never shows an empty box. Each photo frame shows, in this order:
placeholderSrcfrom the photo, when you set it.- For files in
public/: a blurred preview at most 32 px wide, made at build time and inlined as a data URL. Nuxt Photo makes it withsharp, which@nuxt/imagealready installs for IPX. Withoutsharpthere is no preview, and the build logs one warning. Photos fromusePhotoFolderand, withlocalImages, hand-written arrays get it. - For remote images on IPX: a 24 px image from the provider.
- Otherwise the background color
--np-placeholder-bg.
When an image fails to load, the placeholder stays, so the layout does not show an empty frame.
Load the first screen first
Images are lazy by default. Mark the images a visitor sees first, so the browser loads them before anything else:
<template>
<Photo :photo="cover" priority />
<PhotoAlbum :photos="photos" :priority="6" />
</template>priority sets loading="eager" and fetchpriority="high". In Nuxt it
also adds a <link rel="preload"> for the image to the page head, so the
browser starts the download before it reads the rest of the page. At most six
images get a preload. On PhotoAlbum, priority is the number of
thumbnails, counted from the start. Set it to the number of thumbnails in the
first screen; a higher number delays the rest of the page. The first slide of
PhotoCarousel always has high priority.
A <Photo> cannot know its width before the page is shown, so it uses
100vw. When the photo is narrower, say so, or the browser downloads a file
for the full screen width:
<Photo :photo="cover" priority sizes="(min-width: 1024px) 720px, 100vw" />Images below the first screen start loading about one and a half screen heights before they scroll into view, in every browser. The lightbox requests a slide only when it opens. After the open photo has loaded, it loads the previous and the next photo in the background, so the next swipe shows a ready image. With the browser's data saver on, it skips this.
Check the result
Open the network panel, disable the cache, and reload:
- Thumbnails request files close to their displayed width times the screen's pixel density, never wider than the source.
- With Nuxt Image, image URLs go through your provider:
/_ipx/…for IPX,/_vercel/image?…on Vercel. On Vercel, a second load showsx-vercel-cache: HIT. - Images below the first screen are requested as you scroll toward them, and a placeholder is visible until each one is ready.
- If local images work and remote ones fail, add the host to
image.domains.