Images are usually the heaviest part of a page and very often the Largest Contentful Paint element, so they have more effect on load speed than almost anything else you can change. Good image optimization for web means four things together: the right format, the right size for each screen, reserved space to prevent layout shift, and the right loading priority. This guide covers all four, plus how WordPress and Next.js handle them for you.
Choosing a format: AVIF, WebP or JPEG
For photographs and complex images, modern formats deliver the same visual quality at noticeably smaller file sizes than JPEG.
| Format | Best for | Notes |
|---|---|---|
| AVIF | Photos, hero images | Usually the smallest files at a given quality; slower to encode; supported by all current major browsers. |
| WebP | Photos and graphics with transparency | Very broad support, fast to encode, a safe default. |
| JPEG | Fallback | Universal support; use a modern encoder such as MozJPEG. |
| PNG | Screenshots, images needing lossless quality | Often replaceable with lossless WebP. |
| SVG | Logos, icons, illustrations | Vector, tiny, sharp at any size; minify it with SVGO. |
My default is AVIF with a WebP or JPEG fallback for photographic content, and SVG for anything that is really a drawing. Results vary by image, so compare outputs visually rather than trusting a single quality number.
Serving the right size with srcset and sizes
A 2400px-wide hero downloaded by a 390px-wide phone wastes most of its bytes. Responsive images let the browser pick the smallest file that still looks sharp.
<img
src="/images/desk-800.jpg"
srcset="/images/desk-400.jpg 400w,
/images/desk-800.jpg 800w,
/images/desk-1200.jpg 1200w,
/images/desk-1600.jpg 1600w"
sizes="(min-width: 1024px) 50vw, 100vw"
width="1600" height="1067"
alt="Oak standing desk in a bright home office">
srcsetlists the available files and their intrinsic widths (wdescriptors).sizestells the browser how wide the image will be displayed at each breakpoint, before CSS has loaded. Here: half the viewport on large screens, full width otherwise.- The browser multiplies the slot width by the device pixel ratio and picks the best candidate.
The most common mistake is omitting sizes. Without it the browser assumes 100vw and downloads oversized files for images that actually display in a narrow column. Use fixed-density descriptors (1x, 2x) only for images displayed at a fixed size, such as avatars and logos.
Format fallbacks with the picture element
Use <picture> when you want to serve AVIF and WebP with a fallback, or need different crops at different breakpoints (art direction). The browser uses the first <source> it supports; the <img> provides the fallback and carries alt, dimensions and loading attributes.
<picture>
<source type="image/avif"
srcset="/images/desk-800.avif 800w, /images/desk-1600.avif 1600w"
sizes="(min-width: 1024px) 50vw, 100vw">
<source type="image/webp"
srcset="/images/desk-800.webp 800w, /images/desk-1600.webp 1600w"
sizes="(min-width: 1024px) 50vw, 100vw">
<img src="/images/desk-1600.jpg"
srcset="/images/desk-800.jpg 800w, /images/desk-1600.jpg 1600w"
sizes="(min-width: 1024px) 50vw, 100vw"
width="1600" height="1067"
alt="Oak standing desk in a bright home office">
</picture>
If an image CDN negotiates the format automatically (more on that below), you can skip <picture> for format switching and use a plain <img srcset>.
Width and height attributes prevent layout shift
When an image has no dimensions, the browser doesn’t know how much space to reserve, and text jumps down once the image loads. That is Cumulative Layout Shift. Always set width and height to the image’s intrinsic dimensions (or at least its aspect ratio). Modern browsers use them to compute an aspect ratio before the file arrives, and your CSS can still make the image fluid:
img {
max-width: 100%;
height: auto; /* keeps the aspect ratio from width/height attributes */
}
.card-thumb {
aspect-ratio: 4 / 3;
object-fit: cover;
width: 100%;
}
If you are already chasing layout shift, my guide to fixing CLS covers fonts, ads and embeds too.
Lazy vs eager loading and fetchpriority for LCP
Native lazy loading defers off-screen images until the user scrolls near them. It is excellent for images below the fold and harmful for the LCP image, which you want as early as possible.
- Above-the-fold hero / LCP image: no
loading="lazy", and addfetchpriority="high"so the browser fetches it before other images. - Everything below the fold:
loading="lazy"anddecoding="async". - Background images set in CSS are discovered late; if one is your LCP element, preload it or switch to an
<img>.
<!-- Hero: load immediately, high priority -->
<img src="/images/hero-1200.avif" width="1200" height="675"
fetchpriority="high" alt="3D product configurator preview">
<!-- Below the fold -->
<img src="/images/gallery-1.avif" width="800" height="600"
loading="lazy" decoding="async" alt="Configurator colour options">
<!-- Responsive LCP background or late-discovered image -->
<link rel="preload" as="image" fetchpriority="high"
imagesrcset="/images/hero-800.avif 800w, /images/hero-1600.avif 1600w"
imagesizes="100vw" type="image/avif">
Use fetchpriority="high" on one or two images at most. Marking everything high priority is the same as marking nothing.
Image CDNs and on-the-fly transformation
Generating every size and format by hand doesn’t scale. Image services resize, compress and convert on request, cache the result at the edge, and usually pick AVIF or WebP based on the browser’s Accept header. Options include Cloudflare Images and its image transformations, Cloudinary, imgix, and the Vercel or Netlify image services.
<!-- Cloudflare image transformations (must be enabled on the zone) -->
<img
src="/cdn-cgi/image/width=800,quality=75,format=auto/images/desk.jpg"
srcset="/cdn-cgi/image/width=400,quality=75,format=auto/images/desk.jpg 400w,
/cdn-cgi/image/width=800,quality=75,format=auto/images/desk.jpg 800w,
/cdn-cgi/image/width=1600,quality=75,format=auto/images/desk.jpg 1600w"
sizes="(min-width: 1024px) 50vw, 100vw"
width="1600" height="1067" alt="Oak standing desk">
Check each provider’s pricing model, since transformations are often billed per unique variant or request.
WordPress image optimization specifics
WordPress does a lot automatically: it generates multiple sizes on upload, outputs srcset and sizes through wp_get_attachment_image(), adds width/height, lazy-loads content images and, in recent versions, tries to skip lazy loading and add fetchpriority="high" for the likely LCP image. WebP uploads are supported, and AVIF support arrived in a later release if your server’s image library supports it.
Common improvements:
- Use
wp_get_attachment_image()in themes instead of hand-written<img>tags, so you inherit all of the above. - Register image sizes that match your real layout widths.
- Convert JPEG uploads to a modern format with the core output-format filter:
<?php
// Save generated sub-sizes of JPEG uploads as WebP.
add_filter( 'image_editor_output_format', function ( array $formats ) {
$formats['image/jpeg'] = 'image/webp';
return $formats;
} );
// Theme template: responsive, dimensioned, priority hero.
echo wp_get_attachment_image(
get_post_thumbnail_id(),
'large',
false,
[
'loading' => 'eager',
'fetchpriority' => 'high',
'sizes' => '(min-width: 1024px) 50vw, 100vw',
]
);
Check that Imagick or GD on your host supports the target format before switching. More WordPress-specific speed work is in my WordPress Core Web Vitals checklist.
Next.js image optimization specifics
next/image generates srcset, serves modern formats, requires dimensions (or fill), and lazy-loads by default. Enable AVIF explicitly, since the default is WebP only:
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }],
},
};
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/images/hero.jpg"
alt="3D product configurator preview"
width={1600}
height={900}
sizes="100vw"
priority // marks the LCP image; newer versions may name this differently
/>
);
}
Always pass sizes when the image isn’t full width, especially with fill; otherwise Next.js may generate a less efficient set of candidates. The prop for marking the LCP image has changed between major versions, so check the next/image docs for yours. If you deploy outside Vercel, make sure the optimizer has the sharp package available, or configure a custom loader for your image CDN.
Compression tooling: sharp and friends
For build-time processing or a one-off batch, sharp is fast and dependable:
// scripts/optimize-images.mjs
import sharp from 'sharp';
import { readdir, mkdir } from 'node:fs/promises';
import path from 'node:path';
const SRC = 'src/images';
const OUT = 'public/images';
const WIDTHS = [400, 800, 1200, 1600];
await mkdir(OUT, { recursive: true });
for (const file of await readdir(SRC)) {
if (!/.(jpe?g|png)$/i.test(file)) continue;
const name = path.parse(file).name;
for (const width of WIDTHS) {
const img = sharp(path.join(SRC, file)).resize({ width, withoutEnlargement: true });
await img.clone().avif({ quality: 50 }).toFile(`${OUT}/${name}-${width}.avif`);
await img.clone().webp({ quality: 75 }).toFile(`${OUT}/${name}-${width}.webp`);
await img.clone().jpeg({ quality: 78, mozjpeg: true }).toFile(`${OUT}/${name}-${width}.jpg`);
}
}
The Squoosh web app is still handy for comparing formats and quality side by side on a single image. The Squoosh CLI has not been actively maintained for some time, so for automation I’d use sharp, libvips, or your image CDN instead. For SVGs, run SVGO.
Key takeaways
- Prefer AVIF or WebP for photos, SVG for vector graphics, and keep a fallback where needed.
- Use
srcsetwith an accuratesizesattribute so each device downloads an appropriately sized file. - Always set
widthandheightto prevent layout shift. - Never lazy-load the LCP image; give it
fetchpriority="high"and lazy-load everything below the fold. - Let WordPress and
next/imagedo the heavy lifting, but configure formats and sizes yourself. - Automate compression with sharp or an image CDN rather than exporting files by hand.
Frequently asked questions
Should I use AVIF or WebP?
Use AVIF where you can, because it usually produces smaller files at similar quality, with WebP or JPEG as a fallback. If encoding time or tooling is a constraint, WebP alone is still a big improvement over JPEG.
Does lazy loading hurt SEO?
Native loading="lazy" is fine for search engines, since Google renders pages and sees lazy-loaded images. The real risk is lazy-loading the LCP image, which delays it and worsens Core Web Vitals.
What quality setting should I use?
There is no universal number; start around the encoder’s recommended defaults and compare visually on a few representative images. Photos with fine detail usually need higher quality than flat illustrations.
Do I still need the picture element if I use an image CDN?
Usually not for format switching, because most image CDNs pick AVIF or WebP from the browser’s Accept header. You still need <picture> for art direction, where different crops are shown at different breakpoints.
If your pages are weighed down by heavy images and you want them fixed properly across WordPress or Next.js, get in touch and I can set up an image pipeline that stays fast.