How to Cache Images in React, React Native, Next.js, Vue, Angular, Astro & Laravel
Images are typically 50–60% of page weight. Whether you're dealing with a blank-screen flicker on Android, an LCP score that won't budge, or a bandwidth bill that's too high — image caching is almost always part of the fix. This guide covers the actual mechanics per framework, not just "use a CDN."
If you're building a plain React app (Vite, CRA, or any SPA), there's no server in the middle. You can't intercept image requests and process them on the fly. Your entire caching strategy lives in the HTTP headers you send alongside the image file itself.
The most useful header you can set is:
Cache-Control: public, max-age=31536000, immutable
max-age=31536000 is one year in seconds. immutable is the part that actually matters — it tells the browser "this file will never change at this URL, so don't even bother sending a revalidation request." Without it, the browser will still send an If-None-Match request every time the cache expires, even if your image hasn't changed in three years.
The reason this is safe to do is content hashing. Vite outputs hero.3fa9d2.webp instead of hero.webp. When you update the image, the hash changes, the URL changes, and the browser treats it as a completely new file. You get permanent caching and instant updates.
You set these headers at the hosting layer, not in your React code. Here's how it looks for a few common setups:
# Netlify — netlify.toml
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
# Vercel — vercel.json
{
"headers": [
{
"source": "/assets/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]
}
# Nginx
location ~* \.(jpg|jpeg|png|webp|avif|gif|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary Accept-Encoding;
access_log off;
}The second thing worth doing for React apps is preloading your hero image. The browser discovers images late — it has to download HTML, parse it, download CSS, and only then see the <img> tag. By that point you've already wasted several hundred milliseconds. Add a preload hint in your index.html:
<link rel="preload" as="image" href="/assets/hero.a3f9d2.webp" fetchpriority="high" />
This only applies to your LCP (Largest Contentful Paint) image — the biggest visible image on first load. Don't preload everything or you'll make things worse by competing for bandwidth.
loading="lazy" on <img> elements is now supported natively in all modern browsers. Add it to every image that isn't in the initial viewport. It costs nothing to add and the browser handles everything.References: MDN — Cache-Control · web.dev — Preload responsive images
CDN Comparison — Works with Every Framework
Regardless of which framework you're on, putting a CDN in front of your images is the highest-impact change you can make. A CDN caches images at edge nodes around the world, so instead of a 200ms round trip to your server in Frankfurt, a user in Mumbai gets the image from Singapore in under 20ms.
| Service | Best for | Price |
|---|---|---|
| Cloudflare Images | Budget-conscious, global apps | $5/month flat for 100k images |
| Cloudinary | Rich transforms, generous free tier | Free up to 25 credits/month |
| imgix | URL-based API, large scale | Pay per GB of origin + requests |
| AWS CloudFront + S3 | Full control, existing AWS infra | Pay per request + transfer |
One more thing — compress before you cache
A cache only helps repeat visitors. The first request — and every Googlebot crawl — will always download the original file. If that original is a 4MB PNG, no amount of caching will help your LCP score. Use iFixImg to compress and convert to WebP in your browser, no upload required.