Let’s say you want to render 30, 100, 200 images on a SPA web app. What happens if you put all the images in your public folder and deploy the application? The page weight is going to be large for users, meaning that just loading the site itself will take a very long time for users.
So here we have 2 options: we can either optimize the images to a more web friendly compressed format like WebP or upload it into a CDN (Content Delivery Network). Both of these options come with tradeoffs so here are some basics.
Compress images and keep them local
Pros
- Modern codecs like AVIF and WebP typically cut file size by 30 – 70 % compared with JPEG or PNG, so your build artifact shrinks.
- No additional costs
Cons
- Encoding takes more CPU during your build process + compressed files are still part of deployed app so it will inflate your bundle
- Old browsers still need a JPEG or PNG fallback, or at least WebP.
Serve images from a CDN (Content Delivery Network)
Pros
- Keeps the application bundle lean—images are fetched only when requested.
- Edge nodes close to users reduce latency dramatically, especially for global audiences.
- Many CDNs now provide on‑the‑fly transformations: automatic resizing, format negotiation (AVIF/WebP/JPEG), quality tuning, and even blur‑up placeholders.
Cons
- Costs for hosting (though a lot of providers have a free tier)
- Added deployment step of having to upload images separately to CDN
How I uploaded images on my website
All my images are hosted on Cloudinary. Cloudinary has a very generous free tier with storage, real‑time transformations, and a global CDN.
Helper function to optimize each image
// Inject any Cloudinary transformation string right after `/upload/`
const cloudinaryUrl = (src: string, options = 'f_auto,q_auto'): string => {
const [prefix, rest] = src.split('/upload/');
return `${prefix}/upload/${options}/${rest}`;
};
f_auto
lets Cloudinary pick the smallest format each browser understands (AVIF ▶ WebP ▶ JPEG/PNG).q_auto
picks the lowest perceptually “safe” quality level.- Because this happens at request time, I can swap options whenever I feel like it—no re‑deploy needed.
Blur Placeholders
const BLUR_PREVIEW_OPTIONS = 'w_30,q_10,e_blur:300,f_auto';
const blurredPreviewUrl = (src: string) =>
cloudinaryUrl(src, BLUR_PREVIEW_OPTIONS);
Visitors see a low‑res, highly‑blurred version almost immediately (30 px wide, 10 % quality). When the real file finishes downloading it fades in on top. The trick shaves hundreds of milliseconds off Largest Contentful Paint on slow connections.
Responsive srcset so each device pulls only what it needs
const IMAGE_WIDTHS = [400, 800, 1200];
const buildSrcSet = (src: string) =>
IMAGE_WIDTHS
.map((w) => `${cloudinaryUrl(src, `w_${w},f_auto,q_auto`)} ${w}w`)
.join(', ');
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw"
phones download ~400 px wide images, tablets grab 800 px, and desktops top out at 1200 px. That alone can cut 60–70 % of image traffic on mobile.
As always, feel free to contact me if you have any questions about this or how to implement it in your site!