Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 044 / 100

Nuxt Image: what every Vue & Nuxt developer should know

Optimized images by default.

Expona Admin5 min read

Nuxt Image: what every Vue & Nuxt developer should know

Image optimization is one of those details that separates a polished application from one that feels sluggish. Most developers know they should optimize images, but the tooling often feels fragmented—resizing on the server, converting formats client-side, lazy loading libraries bolted on as an afterthought. Nuxt Image eliminates this friction by making optimized images the default behavior, not an extra task.

Why image optimization matters

Before diving into Nuxt Image itself, let's ground why this matters. An unoptimized 5MB JPG on a product page isn't just slow; it compounds across every visitor. On a 4G connection, that single image can block interaction for seconds. Multiply that across a homepage with a hero image, product gallery, and brand assets, and you're watching real users bounce.

Nuxt Image handles the common patterns: serving WebP to browsers that support it, resizing images to actual display dimensions, lazy loading below the fold, and integrating with CDNs to offload processing. It's not magic—it's just the correct defaults applied automatically.

NuxtImg resizes and converts

The <NuxtImg> component is the workhorse. Instead of dropping an <img> tag directly, you use <NuxtImg> and let Nuxt handle the transformation pipeline.

typescript
<template>
  <div>
    <NuxtImg src="/products/sneaker-blue.jpg" alt="Blue sneaker" width="400" height="300" />
  </div>
</template>

What happens behind the scenes:

  • Nuxt detects the original image dimensions if not provided
  • It generates resized versions at multiple widths (e.g., 320px, 640px, 1280px)
  • For modern browsers, it creates WebP variants alongside JPEG
  • The rendered HTML includes a <picture> element with appropriate <source> tags
  • Older browsers fall back to the original JPEG

In practice, your 400px display width no longer loads a 2560px source file. The actual network request might be 45KB instead of 340KB. Multiply that across fifty images on a category page, and you've cut bandwidth by 80%.

You can also transform on-the-fly with modifiers:

typescript
<NuxtImg src="/hero.jpg" alt="Hero" width="1200" height="600" format="webp" quality="80" />

The format parameter forces a specific output format. The quality parameter controls compression—80 is usually a sweet spot where human perception doesn't notice degradation, but file size drops sharply.

Responsive sizes and art direction

Mobile devices have different requirements than desktops. A full-width hero image on mobile might be 100% of a 375px viewport; on desktop, it's 1200px. Serving the same image to both is wasteful.

Nuxt Image integrates with the native srcset and sizes attributes:

typescript
<template>
  <NuxtImg
    src="/article-header.jpg"
    alt="Article header"
    sizes="sm:100vw md:80vw lg:1000px"
    width="1200"
    height="400"
  />
</template>

The sizes prop uses Tailwind breakpoint names. It tells the browser: "on small screens, this image is 100% of the viewport width; on medium screens, 80%; on large screens, 1000px fixed." The browser then selects the appropriate resized variant from the generated set.

For art direction—where you actually want different images at different breakpoints—use the <picture> element directly but still leverage <NuxtImg> as the fallback:

typescript
<template>
  <picture>
    <source media="(max-width: 640px)" srcset="/hero-mobile.jpg" />
    <source media="(min-width: 641px)" srcset="/hero-desktop.jpg" />
    <NuxtImg src="/hero-desktop.jpg" alt="Hero" width="1200" height="400" />
  </picture>
</template>

This pattern is useful for product photography where the mobile framing might spotlight a single shoe, while the desktop shows the full lifestyle context.

Lazy loading and performance

By default, <NuxtImg> applies native lazy loading—the browser's built-in loading="lazy" attribute. Images below the fold don't load until the user scrolls near them.

typescript
<template>
  <div>
    <!-- Hero image, loads immediately -->
    <NuxtImg src="/hero.jpg" alt="Hero" loading="eager" />

    <!-- These load only when scrolled into view -->
    <NuxtImg src="/product-1.jpg" alt="Product 1" />
    <NuxtImg src="/product-2.jpg" alt="Product 2" />
    <NuxtImg src="/product-3.jpg" alt="Product 3" />
  </div>
</template>

The default is loading="lazy". Use loading="eager" only for above-the-fold content that affects Largest Contentful Paint (LCP), which is a Core Web Vitals metric. A typical e-commerce site might mark the hero and top product image as eager, then lazy-load the rest.

Lazy loading pairs naturally with infinite scroll or pagination. A catalog page that loads fifty product thumbnails on demand can now defer loading the bottom thirty until they're needed. This keeps Time to Interactive (TTI) snappy.

CDN providers and static assets

By default, Nuxt Image processes everything through its built-in provider. But in production, you'll often want to offload this to a CDN that specializes in image delivery. Cloudinary, Imgix, and AWS CloudFront all have optimized pipelines.

Configure a provider in your nuxt.config.ts:

typescript
export default defineNuxtConfig({
  image: {
    provider: 'cloudinary',
    cloudinary: {
      baseURL: 'https://res.cloudinary.com/your-cloud-name/image/upload/'
    }
  }
});

Now every <NuxtImg> request routes through Cloudinary:

typescript
<NuxtImg src="products/sneaker-blue.jpg" alt="Sneaker" width="400" height="300" />
<!-- Renders as -->
<!-- https://res.cloudinary.com/your-cloud-name/image/upload/w_400,h_300,f_auto,q_auto/products/sneaker-blue.jpg -->

Cloudinary handles resizing, format conversion, and caching at edge nodes globally. You push the asset once; it's optimized and cached worldwide.

For static assets (logos, icons), you can also preload or inline small images:

typescript
import { useImage } from '#app';

const { getSizes } = useImage();

// Get a preoptimized static asset
const logoSrc = useImage().getSizes('/logo.svg');

SVGs are typically left alone—they're already compact and scale infinitely. Use <img> or <NuxtImg> for SVGs without width/height constraints if the SVG is responsive.

Common mistakes

Not specifying width and height. Nuxt Image can infer dimensions from the source, but this adds latency. Always provide width and height to prevent layout shift and enable optimal srcset generation.

Over-optimizing small decorative images. A 12×12px icon doesn't need resizing and format conversion. Use native <img> or an <SVG> component for these. Save <NuxtImg> for images that users actually perceive.

Forgetting to set loading="eager" for LCP images. If your hero image is lazy-loaded, Largest Contentful Paint delays, and Core Web Vitals suffer. Check your Lighthouse report—if LCP is slow and it's an image, mark it eager.

Ignoring quality settings. Default quality is often 75. For photography, 80–85 is usually imperceptible but saves 20–30% bandwidth. For graphics, you might compress more aggressively. Test with real users.

Not configuring a CDN in production. Nuxt's built-in processing is fine for small projects, but at scale, a CDN is cheaper and faster. Even a hobby side project benefits from offloading image work.

Key takeaways

  • <NuxtImg> automates resizing and format conversion; always use it instead of <img> for photos and product images
  • Responsive sizes reduce bandwidth per device class without manual media query management
  • Native lazy loading defers below-fold images, improving initial load time and TTI
  • CDN providers like Cloudinary add global caching and edge processing; configure one for production
  • Always provide explicit width and height to prevent layout shift and enable optimal responsive variants
  • Mark above-the-fold images with loading="eager" to protect Core Web Vitals
  • Fine-tune quality and format per image type; photography tolerates more compression than graphics
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles