Cutting 24 MB of images out of a portfolio
- Next.js
- Performance
- Images

While rebuilding this site I ran a count on its own image directory and found 24 MB of files, of which four were actually referenced by any page. One of those four was a 3.8 MB animated GIF used as a decorative illustration on the about page.
Nothing here is clever. It is just the audit most projects never get around to running.
Find what is actually referenced
Start with the gap between what exists and what is used. Every file in your public directory, checked against every reference in source:
for f in $(find public/images -type f); do
name=$(basename "$f")
grep -rqF "$name" app lib components || echo "orphan: $f"
doneOn this project that flagged nearly every file. Old project screenshots, three variants of the same logo, a blog image set from a previous version of the site.
One caution before deleting: if image paths can also live in a database — a CMS cover image, an uploaded asset — grep the database too. I checked every collection for /images/ paths before removing anything, which turned a risky cleanup into a safe one.
Pick a format on purpose
The 3.8 MB GIF is the interesting case, because GIF is almost never the right answer. It is capped at 256 colours, compresses photographs terribly, and a modern format does the same job at a fraction of the size.
For the replacements here, WebP at quality 80 landed six full-bleed photographs at 24–96 KB each. The four images actually in use went from roughly 8.2 MB to about 260 KB — a 97% reduction with no visible difference at render size.
If you genuinely need motion, an MP4 or WebM is smaller than the equivalent GIF by an order of magnitude, and <video autoplay muted loop playsinline> behaves like one.
Stop shipping pixels nobody sees
The second habit is serving images far larger than their display size. A 4000px-wide photo in a 400px column is 90% waste.
Decide the real maximum display width, double it for high-density screens, and resize to that. Most content images on this site are 1200–1600px wide, which covers a 2x retina render of anything up to about 800 CSS pixels.
Give next/image the information it needs
next/image handles a lot automatically, but two things still have to be told to it.
`sizes`, whenever you use `fill` or a responsive width. Without it the browser assumes the image occupies the full viewport and downloads the largest candidate on every screen:
<Image
src="/images/projects/vidya.webp"
alt=""
fill
sizes="(min-width: 1024px) 384px, (min-width: 640px) 50vw, 100vw"
className="object-cover"
/>`priority`, on the one image above the fold. Exactly one, usually the hero. Marking several defeats the purpose — everything urgent means nothing is.
Keep the aspect ratio in the layout
Let the container own the ratio — aspect-[16/9] with object-cover — rather than hoping every image arrives correctly proportioned. Layout shift disappears, and swapping a source image later cannot break a page.
What it added up to
The image directory went from 24 MB to under 300 KB, with better-looking images than before. No build tooling was added and no configuration changed. It was a format decision, a size decision, and finally running the orphan check.
That last one is worth scheduling. Unused assets do not announce themselves, and every project accumulates them.