← The Blog

Cutting a portfolio's image weight by 82% without touching the design

6 August 2026 3 min readPerformance

The symptom

My skills page felt slow on mobile data, but the code looked fine. Nothing was obviously wrong: no huge bundles, no blocking scripts of my own, no layout bugs I could see on a fast laptop connection.

So I stopped guessing and measured. I wrote a short script that walked every HTML file, resolved every image reference, and printed the file size next to the size the browser actually rendered it at.

That comparison is where the problem was hiding.

What the numbers said

My certificate scans were between 1122x788 and 1812x1400 pixels, some over 200KB each. Every one of them was displayed in a card measuring 239x148 CSS pixels.

I was shipping roughly seven times the pixels needed, twenty-five times over, on a single page. Together with the badges and internship certificates, that page pulled about 3.1MB of images. One project screenshot elsewhere was a 740KB PNG.

The lesson that stuck: a file is not “big” or “small” in isolation. It is big or small relative to the box it lands in.

The fix, and the constraint I nearly missed

The obvious move is to resize everything down. But each certificate thumbnail is wrapped in a link pointing at its own full-size file, because clicking a certificate should show you the real, readable document. If I had resized in place, every “view certificate” link would have started serving a blurry thumbnail.

So the originals stay exactly where they are. I generated a separate WebP thumbnail for each one and pointed only the <img src> at it. The <a href> still opens the full scan.

certificates/cert-06.jpg   225KB  1812x1400   <- still the link target
thumbs/cert-06.webp         15KB   640x494    <- what the page loads

Across the whole site that took 4.5MB of raster images down to 807KB, an 82% cut, with no visible difference at display size.

The second bug, which the fix created

While I was in there I added width and height attributes to every image. Without them the browser does not know how tall an image will be until it arrives, so everything below shifts down as each one loads.

Adding them broke four images.

The cause is subtle and worth knowing. Several images are sized by CSS with width: 100% and an aspect-ratio, and no height. The HTML height attribute becomes a presentational hint, and because no CSS rule set a height, nothing overrode it. With both a definite width and a definite height, the browser ignores aspect-ratio entirely. A photo that should have been 428x337 rendered at 428x1201.

The fix is one line, and it is the reason it belongs in a base rule rather than sprinkled per-image:

img { height: auto; }

Now the attribute still reserves the right space before load, but CSS decides the final size. Class-based rules like .cert-card img { height: 120px } still win on specificity, so nothing else changed.

Proving nothing moved

A refactor that touches 65 images across 13 pages is exactly the kind that quietly breaks one layout you do not look at. So before changing anything I recorded the rendered box of every image on every page, plus each page’s full scroll height. After the change I measured again and compared.

Twelve of thirteen pages came back pixel-identical. That is the part I would repeat on any visual refactor: decide up front what “unchanged” means, capture it as numbers, and diff.

Takeaways

  • Compare intrinsic size against rendered size. That single ratio finds most image waste.
  • Check what links to a file before you shrink it. A thumbnail and a full-size asset are two different jobs.
  • width and height attributes are presentational hints, not decoration. Pair them with height: auto.
  • Measure before, measure after, diff. “It looks fine” is not verification.

Comments

Comments run on GitHub Discussions through giscus, so there is no database here and no account details for me to hold.

To switch it on: make the repo public, enable Discussions, install the giscus app, then set giscus.repo in src/lib/site.ts. Until then this page makes no third-party request.