Skip to main content
All posts
Next.js6 min read

Next.js Image Optimization Doubles Your Build Time on a Large Gallery

A photography page with four hundred images turned a two minute build into a fourteen minute one. The image component was not the problem. Asking it to do the work at build time was.

Portrait of Benjamin Fazli, the author of bfzli.com

Benjamin Fazli

Principal EngineerSkopje, North Macedonia

Gallery wall of framed collage boards lit by spotlights

The symptom

A gallery route rendered around four hundred images through the framework image component. Builds went from just over two minutes to fourteen, and on a smaller build container they failed outright after exhausting memory.

What was happening

Every statically rendered image needs a placeholder and a set of resized variants generated ahead of time. With a blur placeholder requested on each one, the build was decoding four hundred source files, producing several sizes each, and generating an inline preview for every single one. All of it during the build, all of it competing for the same two cores.

The important realisation is that a visitor sees roughly nine of those images before scrolling. The build was doing four hundred images of work to serve nine.

The fix

Only the images above the fold get priority treatment. Everything else loads lazily, which is already the default:

jsx
<Image
    src={photo.src}
    alt={photo.alt}
    width={800}
    height={600}
    priority={index < 6}
    sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
/>

Drop the blur placeholder on the long tail. It is a genuinely nice detail on a hero image and an expensive one repeated four hundred times. I kept it on the first row and used a plain background colour underneath the rest.

Set `sizes` accurately. Without it the framework generates variants for every configured breakpoint. With it, only the widths that can actually be requested are produced. This alone removed a large share of the generated files.

Reduce the configured widths to the ones the layout can use. The defaults cover a wide range of devices that this particular layout never renders at:

ts
const nextConfig = {
    images: {
        deviceSizes: [640, 828, 1200, 1920],
        imageSizes: [256, 384]
    }
}

Paginate the gallery. Thirty six images per page was the single largest improvement, and it made the page better to use as well. Nobody was scrolling through four hundred photographs in one column.

Build time settled at three minutes and ten seconds, and largest contentful paint improved as a side effect, because the browser was no longer parsing markup for hundreds of off screen images.

The principle

Optimisation at build time is not free, it is prepaid. That is a good trade for a hero image on every page and a bad one for content most visitors never scroll to. Decide per image, not per project.

If a build slows down sharply after a content change rather than a code change, count the assets. The answer is usually a number that grew while nobody was watching it.