Skip to content
Focal
Blog
  • image-processing
  • architecture
  • performance

Resizing images at build time vs on request, and how to choose

Compare resizing images during the build with resizing them on first request, covering build length, file counts, cache misses, new sizes and encoding cost.

By Focal team 5 min read

Every responsive image needs one file per width, and often one per format. A team can create those files while the site builds, or it can create each one the first time a browser asks for it. This post explains how each approach works in current tools, counts the files involved for a real catalogue size, and lays out when each one fits.

How build-time resizing works

A build step reads each source image, writes every width and format your templates reference, and deploys the results as static files. Astro works this way for prerendered pages. Its documentation states that the <Image /> component transforms images “at build time for prerendered pages”. The output gets a content hash in its file name, such as /_astro/my_image.hash.webp.

Astro keeps processed images in a cache directory, ./node_modules/.astro by default. If your build system preserves that directory between builds, Astro reuses the processed files and skips the work. Astro copies files in the public/ folder as they are, with no processing.

The content hash matters for caching. Browsers and CDNs can cache a file name that changes with its content for a year. MDN calls this the cache-busting pattern and gives Cache-Control: max-age=31536000, immutable as the header for such assets.

How on-request resizing works

An on-request service puts the transformation in the URL. On the first request for a URL, the service fetches the source, transforms it, returns it and stores the result. Later requests for the same URL get the stored copy.

Next.js does the same inside the framework. Its image component resizes images on demand and caches them in <distDir>/cache/images. The cache lifetime is the larger of the minimumCacheTTL setting, which defaults to 14,400 seconds (4 hours), and the source image’s own Cache-Control header. The docs state that there is no mechanism to invalidate that cache.

Next.js does not resize images during a static export. Its docs list image optimisation with the default loader as unsupported under output: 'export', and they point you to a custom loader that builds URLs for an external image service.

web.dev describes image content delivery networks (CDNs) as services that transform, optimise and deliver images, controlled through the URL. Many can fetch images from where they already live instead of requiring an upload.

Count the variants first

The deciding number is how many files the build would have to create. Multiply four values:

products × photos per product × widths × formats = files
4,000    × 4                  × 5      × 2       = 160,000

A build-time pipeline creates all 160,000 files before each deploy, or reads them from its cache. It creates them whether or not any visitor ever loads the fifth photo of a discontinued product. An on-request service creates a file only when someone requests that exact URL.

Now add a sixth width because the design team changed the grid. The build-time pipeline has to create 32,000 new files before the next deploy. The on-request service creates each new size the first time a visitor asks for it.

The trade-offs side by side

Question Build time On request
When does encoding happen? During the build On the first request for each URL
Does the first visitor wait? No Yes, on a cache miss
What does a new width cost? A new file for every image, before the deploy A new file for each image when first requested
Who can add images? Anyone who can trigger a build Anyone who can upload to the source storage
What runs in production? Static file hosting A resizing service
How do you replace an image? A new build writes a new hashed name Upload under a new path, or wait for the cache to expire

When build time fits

Build-time resizing suits images that live in the repository and change with the code. Marketing pages, documentation and blog posts are typical cases. It also suits teams that want nothing but static hosting in production. The hashed file names give you a year-long cache lifetime with no extra work.

It becomes a problem when the file count grows. Every width and format multiplies the build’s work, and a cold cache on a fresh build machine repeats all of it.

When on-request fits

On-request resizing suits images that change without a deploy. A merchandiser uploads new product photos to a bucket, and the storefront can show them at every size as soon as the page references them. It also suits large catalogues, where visitors may never request many combinations of photo, width and format. And it suits teams that change sizes often, because a new width costs nothing until someone requests it.

The costs move to request time. The first request for each URL waits for the source fetch and the transformation. A new width rolled out across a catalogue creates a burst of these misses at once. Slow encoders make each miss longer. The Next.js docs state that AVIF generally takes 50% longer to encode than WebP.

Replacing an image also needs care. A cached result stays valid until its lifetime runs out. Next.js has no invalidation mechanism, so its docs suggest changing the src or deleting the cache files. The simplest rule for any on-request setup is to upload a changed image under a new path.

You can mix both

The two approaches do not exclude each other. A site can build its logos, icons and editorial images into the deploy, with hashed names and year-long caching. The same site can serve catalogue and user-uploaded images from an on-request service that reads from the storage bucket. The dividing line is who changes the image and how often.

Where Focal fits

Focal is an on-request service. You point it at storage you already own, such as an Amazon S3, Google Cloud Storage or Cloudflare R2 bucket or your own server, as long as it is publicly readable over HTTPS. Each URL describes the output, for example https://img.focaltool.com/your-key/unsafe/800x0/filters:quality(80)/products/chair.jpg, and Focal caches each result at the edge. Focal prices by workers, which is how many images it processes at the same time for your account. Cache hits use no worker. Plans start at $449 per month for 8 workers, with a 14-day free trial. The pricing page has the details.

To decide for your own site, run the formula above with your real numbers. A result in the hundreds, from images that only change when you deploy, belongs in the build. A result in the tens of thousands, from images that staff upload every day, fits on-request resizing better.

Sources

More from the blog

All posts