Aerolab Field NotesEngineeringNo. 001 · Sep 2026

Measure the bytes before you fix them

Four performance problems on our own website, and how each one looked different once we measured it.

Written by
Gonzalo BiluneHead of Engineering, Aerolab
Published
Reading time
4 min read

The short version

  1. A video poster that points at the video file makes the browser download it as an image and throw it away. On our homepage that cost 11.8 MB before anyone scrolled.
  2. If your CMS bills per cache miss, every image width is its own bill. Resize on your own origin, behind a cache, instead of generating a URL per width.
  3. Ask for the image format you want. Our CMS was sending our optimizer a JPEG copy of a WebP original, which then got compressed again.
  4. A font subset that drops OpenType features can be five times smaller and wrong. Check what your CSS turns on before celebrating.

Most performance work starts with a hunch. The page feels heavy, a score dips, somebody remembers reading that a certain attribute is slow. The rule we follow on this website is less exciting: measure the bytes before calling something a problem, and measure again before calling it fixed.

It sounds obvious written down. We still broke it, or came close, four times on aerolab.co, and each time the numbers told a different story than the hunch.

The poster that downloaded the video

The project cards on our homepage play a short video: on hover on desktop, and when they scroll into view on phones. At some point the cards got poster={src}#t=0.1. The idea was reasonable. Point the poster at the video file with a time fragment, and the browser paints the first frame as a placeholder.

It doesn't. The browser fetches the poster URL as an image, fails to decode an MP4 as an image and throws the bytes away. We measured it on production: roughly a megabyte per card, 11.8 MB before any scrolling, while every video element reported zero bytes buffered, no duration and a ready state of zero. None of that data ever reached the video player.

Downloaded as 'images' before anyone scrolled
11.8 MB
What the video elements actually buffered
0 bytes

To be sure it was the poster and not the video, we isolated it with two detached video elements, both set to preload="none". The one with only a poster transferred 256 KB and kept climbing. The one with only a source transferred nothing. Poster loading ignores preload, which is why the download happened whatever that attribute said.

The fix was to remove the poster and keep preload="none". Chrome downloads nothing for an offscreen video at any preload value, so hover and scroll playback didn't change. The none is there for Safari, which is more eager. If the cards ever stutter, the answer is a real poster image, not the video URL again.

One image, a dozen bills

Our images come from Sanity, and the expensive part is bandwidth on cache misses. We proxy those images through our own domain so Cloudflare caches them, which means Sanity serves each image roughly once.

The trap is that the cost is per distinct URL, not per image. next/image asks for a different width for each device size. Give it a loader that builds a Sanity URL per width and every source image turns into a dozen URLs, each one its own cache entry and its own trip to Sanity. On our homepage, 13 source images would have become 154 distinct URLs.

So resizing happens on our origin. Next's image optimizer fetches each source once through the cached proxy and re-encodes it locally. Caching the optimizer's output at the edge needed its own rule, but the expensive part stays cheap.

Ask for the format you want

This one we only caught by comparing files. Sanity reads the Accept header and sends a JPEG to any client that doesn't say it accepts WebP. Our image optimizer didn't say so. It was receiving JPEG copies of images we had uploaded as WebP and compressing them again: three lossy generations where one would do.

The numbers on one cover image: 84,166 bytes as the WebP we uploaded, 145,751 bytes as the JPEG Sanity sent instead. To prove which input the optimizer had used, we compared its output against both candidates. The one it came from scores several dB higher.

The fix is one header. Our proxy now asks for WebP on image requests that don't name a format, and leaves clients that do name one alone. Asking is safe because Sanity only ever downgrades: JPEG and PNG originals come back byte-identical either way.

Measure the bytes before calling something a problem, and measure again before calling it fixed.

From the engineering guidelines in our repository

The font that got smaller and wrong

We subset every font we ship down to the characters we use. A naive subset of ABC Repro, our brand typeface, comes out at 17 KB instead of 95 KB. It looks like a great result until you notice the type is wrong.

Our global CSS switches on a set of stylistic alternates, ss01 through ss12, and those alternates carry the letterforms that make ABC Repro look like our brand. The naive subset drops them. Every heading on the site changes shape, and nothing reports an error.

Now a script does the subsetting. It keeps any OpenType feature that is on by default or turned on somewhere in our CSS, and it checks its own output: no character or feature that was asked for may go missing. Done this way, the ABC Repro cut used on the Dreamer case study went from 425 KB to 208 KB with every letterform intact.

The output files also carry a content hash in their names. Our static files are cached as immutable for a year, and regenerating a font under an old name once left browsers and the CDN serving the stale file. With a hash, different bytes always mean a different URL.

What we check now

A few habits came out of this, and they apply well beyond our own site:

  • Report cold-cache numbers. A second run serves from disk cache, shows zero transfer for almost everything and flatters whatever you just changed.
  • Separate one-off costs from real ones. On localhost, the first request for an optimized image pays for the optimization. Production behind a CDN doesn't.
  • Trust findings that survive both runs: duplicated requests, preloads nobody uses, the wrong responsive image.
  • A cached 200 doesn't prove the origin is up. Add a unique query string to reach it.

None of these fixes came from an audit. They came from somebody opening the network panel, writing the numbers down and refusing to trust the first explanation. That habit costs nothing, and I'd rather hire for it than for knowledge of any particular framework.

Questions, answered

Does a video poster attribute download the video file?
If the poster URL points at the video file, yes. The browser requests it as an image, fails to decode it and discards the bytes, and poster loading ignores the preload attribute. Use a real image for the poster, or leave it out.
Should next/image use a Sanity loader?
Only if you are happy to pay for every width. A loader turns each source image into one URL per device size, and each URL is its own cache miss. We resize on our own origin, behind a cached proxy, so Sanity serves each image about once.
Why did my font subset change how the text looks?
The subset probably dropped OpenType features your CSS switches on, such as stylistic sets. Keep every feature that is on by default or enabled in your CSS, and check the output against the source font.
About the author

Gonzalo Bilune

Head of Engineering, Aerolab

Gonzalo (Bilu, to the team) leads engineering at Aerolab. Before joining, he built fintech and e-commerce products at Mercado Libre, Ualá and Pomelo.

LinkedIn ↗
Filed under

Engineering · Web performance · Next.js · Images · Video · Web fonts · Caching

More Field Notes