Performance

Image Optimisation Without a Paid CDN

Images are where page weight comes from. On a text site the HTML and CSS together might be 60 KB; a single unoptimised photograph can exceed that several times over. If your bandwidth bill or your Largest Contentful Paint has a problem, images are almost always where it started.

The good news is that the largest win is also the least technical, and it has nothing to do with picking between file formats.

Measure before you change anything

Open the page in a browser, open the network panel, reload with the cache disabled, and sort by size. Read the transferred column, not the uncompressed one.

You will usually find that two or three files account for most of the total, and that they are far larger in pixel dimensions than anywhere they are displayed. That second observation is the whole article.

The real problem is dimensions, not format

Consider a 3000-pixel-wide photograph rendered in an 800-pixel column.

Converting that JPEG to WebP saves something in the region of 25–35 percent. Resizing it to 800 pixels wide saves around 90 percent, because you are discarding pixels that were never going to be displayed. Do both and you get the 90 percent plus a slice of the remainder.

Format changes are what everyone writes about because they are a single setting somewhere. Resizing is what actually moves the number, and it requires knowing your layout.

Serve each image at roughly the size it is displayed at, doubled for high-density screens, and no larger. A 1200-pixel source in an 800-pixel column is correct. A 3000-pixel source in the same column is a mistake that survives every format migration you will ever run.

Then pick a format

Once dimensions are right, the format choice is straightforward:

  • WebP for photographs. Broad browser support, and typically 25–35% smaller than an equivalent-quality JPEG.
  • AVIF when you want to go further. Files can be half the size of JPEG at similar quality, and encoding takes noticeably longer — which matters if you are encoding at build time on every deploy.
  • PNG only for images that genuinely need lossless or transparency, such as screenshots with text. A photograph saved as PNG is usually several times larger than it needs to be.
  • SVG for logos, icons and diagrams. It scales, it is tiny, and it is text that compresses well.

The practical pattern is to encode WebP as the default and add AVIF as a progressive enhancement behind a <picture> element if your build can afford the encode time. Do not skip the resize step to buy time for the AVIF encode — that is optimising the wrong variable.

Lazy loading, with one exception

loading="lazy" on an <img> defers the fetch until the image approaches the viewport. It is one attribute and it removes most below-the-fold image weight from the initial load.

The exception matters: never lazy-load your Largest Contentful Paint image. That is usually the first large image near the top of the article. Deferring it delays the very thing you are being scored on. Mark it up the other way:

<img src="/hero.webp" width="1200" height="800"
     loading="eager" fetchpriority="high" alt="…">

Everything below it gets loading="lazy". This one distinction is worth more than most image CDN subscriptions.

Always declare the dimensions

<img src="/photo.webp" width="1200" height="800" alt="…">

Browsers derive the aspect ratio from these attributes and reserve the space before the image loads. Without them, the image expands on arrival and pushes everything below it down — a layout shift, measured on every pageview.

This is the same mechanism that makes reserved ad slots matter, and it costs nothing to get right.

Doing it all at build time

For a static site you do not need a runtime image service. Everything above can happen during the build, which means the transformation runs once rather than on every request.

  • Framework image components. Astro, Next and Nuxt all ship one that resizes, re-encodes, and generates srcset from a single source file. If you are on a framework with this built in, the work is already done — you just have to use the component instead of a raw <img>.
  • sharp directly. For a build script or a one-off batch, a short Node script that walks a directory, resizes to a maximum width, and writes WebP is about twenty lines. It is deterministic and it runs offline.
  • Build-time srcset. Emit three or four widths (say 480, 800, 1200, 1600) and let the browser pick. The markup is verbose; generating it is not.

The trade is that the build gets slower and the repository stores derived files if you commit them. Neither matters much for a site that deploys a few times a week. Both would matter for a site where authors upload images through a CMS and expect instant previews, which is the case where a runtime image service genuinely earns its fee.

When a paid CDN is the right answer

Not never. A few situations justify it:

  • Images are uploaded at runtime by users, and you cannot predict the dimensions you will need.
  • The site has thousands of images and the build time has become the bottleneck.
  • You need on-the-fly transformation for arbitrary crops or overlays that no build step can enumerate.

For a content site with a hundred articles and a few images each, none of that applies. Resize at build time, declare your dimensions, lazy-load everything below the fold, and keep the LCP image eager. That is the whole job, and it costs nothing.

The order to do this in

  1. Measure. Find the two or three files that dominate.
  2. Resize them to the dimensions they are displayed at.
  3. Re-encode to WebP.
  4. Add loading="lazy" to everything except the LCP image.
  5. Add width and height everywhere.
  6. Re-measure and confirm you moved the number.

Doing step 3 before step 2 is the most common mistake in this area, and it is why sites run expensive image pipelines and still load slowly.


Written by TestedHost. Every recommendation on this site comes from running the setup described, on a live deployment — not from a vendor spec sheet. Spotted something out of date? Tell us.