Images are still one of the easiest ways to wreck Largest Contentful Paint.
You can have a clean React app, a tidy component tree and a decent bundle size, then lose the whole page to one 2400px hero image that loads lazily from a slow origin. I have seen that exact problem more times than I would like.
The annoying part is that "use an image CDN" is not always the answer. Sometimes next/image is enough. Sometimes a CDN is exactly what you need. Sometimes the real fix is boring: set the right sizes, stop lazy-loading the hero image, and stop letting CMS uploads go live without constraints.
This guide is for choosing the right image optimization setup without buying more tooling than the site needs.
The most important thing is to separate delivery problems from markup problems. A better image service can generate smaller files, but it cannot fix a hero image that is lazy-loaded, hidden behind client-side rendering, missing dimensions, or requested with the wrong sizes value.
The image audit order I use
Before picking a tool, audit one important page and answer these questions:
| Step | Question | What it tells you |
|---|---|---|
| 1 | What is the LCP element? | Whether images are actually the bottleneck. |
| 2 | Is the LCP image in the initial HTML? | Whether the browser can discover it early. |
| 3 | What image dimensions are downloaded on mobile? | Whether srcset and sizes are correct. |
| 4 | What format is delivered? | Whether WebP/AVIF/JPEG fallback is working. |
| 5 | Are width and height set? | Whether the image can cause CLS. |
| 6 | Is the asset cached near users? | Whether a CDN or hosting change would help. |
Only after that audit should you choose between framework-native optimization, an image CDN, or build-time compression.
Start with the actual image problem
Do this before choosing a tool. Work out which problem is hurting the page:
- Files are too large.
- Mobile users get desktop-sized images.
- JPEG or PNG is being used where WebP or AVIF would be smaller.
- The browser discovers the hero image too late.
- The LCP image is lazy-loaded or not prioritised.
- Images are transformed repeatedly instead of cached.
- Editors can upload anything and the frontend has no guardrails.
Compression tools help with file size. Image CDNs help with sizing, formats, caching and transformations. Framework image components help with responsive markup and layout stability.
None of them fix a hero image that only appears after client-side JavaScript runs.
Quick picks
- Next.js on Vercel or a compatible host: start with
next/image. - React SPA or Vite site: use build-time optimization for local images or an image CDN for CMS assets.
- Astro/content site: use Astro's image tooling for local files and a CDN for media libraries.
- WordPress: use a plugin/CDN workflow, but check the LCP image after enabling it.
- Agency/client sites: pick the option that prevents non-developers from accidentally uploading a 4 MB homepage banner.
Decision matrix
| Situation | Best first choice | Why |
|---|---|---|
| Next.js app with local images | next/image | Good defaults, responsive output, layout stability. |
| Next.js app with many CMS images | next/image plus strict remotePatterns, or an image CDN | Remote sources need predictable transforms and cache behavior. |
| React SPA with static images | Build-time Sharp/Squoosh pipeline | No server image optimizer required. |
| React SPA with user/editor uploads | Image CDN | Runtime resizing and format conversion matter. |
| Multi-site brand or agency workflow | Cloudinary/ImageKit/Imgix-style platform | Shared media management and presets reduce editor mistakes. |
| Small static content site | Build-time compression | Boring, cheap, repeatable. |
The wrong choice is usually the one that solves an operations problem you do not have yet.
Next.js Image component
Next.js already has a good image story through the Image component. It can create responsive variants, lazy-load non-critical images and reserve space to reduce layout shift.
Use it first when:
- the site is already on Next.js
- images are local or come from known remote domains
- your host supports the optimization flow you need
- you do not need a shared media library across several sites
Example:
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero-dashboard.jpg"
alt="Dashboard preview"
width={1200}
height={700}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
);
}
The small details matter:
- Use
priorityfor the real LCP hero image. - Add an accurate
sizesvalue. The default can be too broad for real layouts. - Do not hide critical images behind components that only render after client state loads.
- Configure
remotePatternsproperly for CMS images.
Where it gets awkward:
- complex editorial cropping
- lots of remote CMS media
- transformations shared across multiple products
- art direction, overlays or reusable image presets
- non-Next.js sites in the same business
If the image workflow is bigger than one Next.js app, reach for a CDN.
Next.js hosting caveat
The Image component depends on the hosting/runtime path supporting the optimization mode you choose. On some static exports or custom deployments, you may need a custom loader, pre-generated images, or an external image CDN.
Check this early:
- Does production actually serve optimized image URLs?
- Are remote images allowed by
remotePatterns? - Are cache headers stable?
- Does the image optimizer run at the edge, serverless, or build time?
- What happens when a CMS editor replaces an image at the same URL?
Do not assume local development behavior matches production.
Cloudinary
Cloudinary is the heavyweight option. It handles image and video storage, transformations, responsive delivery, cropping, overlays and asset management.
It makes sense when media is part of the product workflow, not just a folder of static images.
Use Cloudinary when:
- editors upload a lot of media
- you need transformations, crops, overlays or art direction
- multiple sites share the same assets
- you need image and video handling together
- non-developers need a media UI
Example URL pattern:
<img
src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_800/sample.jpg"
width="800"
height="533"
alt="Optimized sample"
/>
f_auto lets Cloudinary choose the format. q_auto handles quality. The width parameter stops you sending the original monster file to every device.
Watch the usual trade-offs: pricing, transformation sprawl, account limits and keeping image presets consistent across the team.
For teams, Cloudinary works best when you define named transformations or helper components instead of letting every template invent URL parameters. That keeps quality, crop, and width decisions consistent.
ImageKit
ImageKit is a focused image CDN with URL-based transformations, automatic format conversion, quality settings and media management.
It is a good fit when you want image delivery improvements without building your own pipeline.
Use ImageKit when:
- images come from a CMS, S3 bucket or existing origin
- you want global delivery and transformations
- you want to keep existing asset URLs mostly intact
- you need a lighter image platform than Cloudinary
Typical URL pattern:
<img
src="https://ik.imagekit.io/example/site/hero.jpg?tr=w-800,q-80,f-auto"
width="800"
height="533"
alt="Optimized hero"
/>
The main thing to check is whether your generated widths match the sizes values in your frontend. A CDN can generate perfect variants and still waste bandwidth if the HTML asks for the wrong one.
ImageKit is often a good middle path for sites that already have assets in a bucket or CMS and mainly need faster delivery, automatic formats, and simple transformations.
Imgix
Imgix is a developer-friendly image CDN built around URL parameters and precise transformations. It is useful when the engineering team wants control and the source images already live somewhere else.
Use Imgix when:
- images live in S3, a CMS or another origin
- you need cropping, DPR variants and format control
- engineers are comfortable managing transformation URLs
- you want a mature CDN workflow without a full DAM-style setup
The risk is complexity leaking into templates. If every component invents its own transformation string, the site becomes harder to maintain. Create presets or helper functions early.
Example helper shape:
export function productImage(src: string, width: number) {
return `https://images.example.com/${src}?w=${width}&auto=format,compress`;
}
The helper is less about saving lines of code and more about preventing hundreds of slightly different transformation strings.
Cloudflare Images, Image Resizing and Polish
Cloudflare has a few image options, depending on your plan and setup. Cloudflare Images can store and transform images. Image Resizing can transform assets at the edge. Polish can optimize images that pass through Cloudflare on supported plans.
Use Cloudflare when:
- the site already sits behind Cloudflare
- you want fewer vendors
- your image needs are fairly straightforward
- edge delivery matters more than media-library features
Check plan availability before designing around a feature. Also check cache behaviour carefully, especially when images are replaced but URLs stay the same.
ShortPixel, TinyPNG and build-time compression
Not every site needs an image CDN.
For small static sites, build-time compression may be the sanest option. Run images through Sharp, Squoosh, ShortPixel, TinyPNG or a similar tool before deploy. Commit the optimized files. Move on.
That works well when:
- there are not many images
- images live in the repo
- content changes slowly
- editors are not uploading new media every day
- transformations on demand would be overkill
A simple Sharp script that creates WebP/AVIF variants and resizes large originals can beat a paid CDN for a small developer-owned site.
Example build-time policy:
| Image type | Max width | Format |
|---|---|---|
| Hero | 1200-1600px | AVIF/WebP plus JPEG fallback if needed. |
| Article inline | 900-1200px | WebP or AVIF. |
| Thumbnail/card | 400-800px | WebP. |
| Logo/icon | Source SVG when possible. |
The exact numbers depend on your layout, but the policy should exist. Without guardrails, image optimization becomes a one-off chore instead of a repeatable publishing workflow.
The LCP image checklist
Before blaming your image service, check the page itself:
- Is the LCP image the right size for mobile?
- Is it present in the initial HTML?
- Is it prioritised with
priority,preloadorfetchpriority="high"where appropriate? - Is it accidentally lazy-loaded?
- Are
widthandheightset? - Is it a CSS background image that the browser discovers late?
- Is TTFB slow before the image even starts loading?
- Are third-party scripts or heavy JavaScript delaying render?
Most bad LCP images fail more than one of those checks.
Responsive sizing mistakes
The most common image waste I see in React and Next.js is a wrong sizes attribute.
This is vague:
<Image
src="/article-card.jpg"
alt="Article preview"
width={1200}
height={800}
sizes="100vw"
/>
If the image is actually a three-column desktop card, 100vw can tell the browser to download a much larger candidate than needed.
Better:
<Image
src="/article-card.jpg"
alt="Article preview"
width={1200}
height={800}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
Match sizes to the rendered CSS layout. Then confirm in DevTools Network which candidate the browser downloads on mobile and desktop.
Example hero image markup
For a non-Next.js site:
<link
rel="preload"
as="image"
href="/images/hero-1200.webp"
imagesrcset="/images/hero-600.webp 600w, /images/hero-1200.webp 1200w"
imagesizes="100vw"
/>
<img
src="/images/hero-1200.webp"
srcset="/images/hero-600.webp 600w, /images/hero-1200.webp 1200w"
sizes="100vw"
width="1200"
height="700"
fetchpriority="high"
decoding="async"
alt="Product dashboard showing performance metrics"
/>
Preload one image carefully. Do not preload every image above the fold and hope for the best. Preload the actual LCP candidate, then check the waterfall.
For React apps where the hero is selected from data, render a stable fallback image or server-render the selected asset when possible. If the image URL is only known after client-side JavaScript runs, the browser cannot start early.
CMS and editor guardrails
Tools do not protect performance unless the publishing workflow uses them.
For CMS-backed sites, define:
- allowed upload dimensions
- required alt text
- required crop ratios for hero/card/social images
- automatic WebP/AVIF conversion
- cache invalidation rules when an image is replaced
- a maximum original file size
For marketing teams, the best image optimization tool is often the one that prevents bad uploads before they reach production.
Cost, lock-in and failure modes
Image CDNs are usually worth it for media-heavy sites, but they introduce operational trade-offs:
| Risk | How to reduce it |
|---|---|
| Transformation URLs spread through templates | Use helper functions or shared image components. |
| Vendor costs grow with traffic or transformations | Cache aggressively and limit variant sprawl. |
| Replaced images stay cached | Use versioned URLs or explicit invalidation. |
| Original images disappear from the source CMS | Keep source-of-truth storage clear. |
| Tool-specific syntax leaks into content | Store clean asset IDs/URLs and transform in the frontend layer. |
These are not reasons to avoid image CDNs. They are reasons to set boundaries before the image layer becomes part of every template.
How to compare options fairly
When testing two tools, use the same page and compare:
- LCP on mobile.
- The exact LCP image URL and downloaded candidate size.
- Transfer size and encoded size for the image.
- Cache headers.
- CLS from image dimensions or placeholders.
- Production behavior, not only local development.
Do not compare only "original image size vs compressed image size." Users experience discovery timing, network latency, cache behavior, decode time, and render delay too.
Which option should you choose?
Choose framework-native optimization when the stack already supports it, image sources are simple and you do not need cross-site media management.
Choose an image CDN when images come from a CMS, multiple sites share assets, non-technical users upload media, or image traffic is commercially important.
Choose build-time compression when images live in the repo and content changes slowly. It is boring, cheap and often enough.
If you are still unsure, start with the option that is easiest to remove:
- Fix markup first: dimensions,
sizes, priority/preload, LCP discovery. - Add framework-native optimization if your stack supports it.
- Add build-time compression for local assets.
- Move to an image CDN when the workflow or traffic justifies it.
Related PageSpeedFix guides
Bottom line
For many Next.js sites, next/image is the right first move. For CMS-heavy sites, agencies and ecommerce, an image CDN is usually worth it. For small static sites, a build-time script may be all you need.
Do not buy a tool before fixing loading priority, responsive sizes and LCP discovery. Those mistakes can wipe out most of the benefit.