Next.js Image Optimization
Next.js Image Optimization
TL;DR - for optimal speed and performance when loading collections of images, a CDN between the user’s browser and next.js is necessary.
I have a web application where I’m loading a collection of images (think image feed) and ran into a case where the load time of images was not only unreasonably long, but when hitting the same image collection page from multiple accounts, each account had to bear witness to the same insufferable tragedy of the images slowly popping into view.
.. next.js comes with fetch() and caching in combination with SSC, but I (naively) though that because the server itself was not delivered with the client app, and it was instead part of the shared hosted service, that the data fetching for one would take care of it for the rest. .. next.js resizes on the fly though and sends the result to the browser to cache, the next.js server itself does not cache .. a CDN between GCP and the next.js server wouldn’t help, since GCP already serves the images fast (that’s not the bottleneck)
It was odd to find the collection of images loading within 1-2 seconds in my local environment, but in staging the same collection of images took 10-30 seconds.
![]() |
![]() |
![]() |
Not only were the image load times egregious, but some images returned a 503 error (“Service unavailable”), suggesting there was a bottleneck wherein the server could not handle all the incoming requests.
To see if next.js and its image optimization strategy was the culprit, I added the `unoptimized` prop to the `<Image />` component and immediately saw the load time for the image collection in staging drop from the original 10-30 seconds to 1-2 seconds.
![]() |
![]() |
![]() |
<Image /> fetching with the `unoptimized` prop When we use the next.js <Image /> component with the `unoptimized` prop, the browser fetches the file directly from GCP, bypassing any resizing or compression. While we don’t pay any extra cost outside of the standard storage + egress for the direct download from GCP, our user takes the UX hit in having to download the full source, which in our case can be expensive (250kb - 6mb).
By disabling cache in the network tab, we can forcibly load images directly from the server instead of utilizing the browser’s memory cache.
<Image /> fetching without the `unoptimized` prop When we use the next.js <Image /> component without the `unoptimized` prop, next.js will intercept the image request, fetch the original file from GCP, perform the resizing/compression, and then return the result to display to the user. The bottleneck here is that if we’re requesting many images at once, especially large images, we need to allocate enough resources in GCP to concurrently handle the request load.
There are clear reasons to use the next.js <Image /> component over the HTML <img> element (which <Image /> extends):
- next.js images are translated to the optimized `.webp` image format and image sizes are correctly sized dependent upon the device they’re being viewed from
- next.js prevents layout shifting while images are loading
- next.js uses native browser lazy loading, only loading images when they enter the viewport
This brings up the refresher of why image optimization is important:
References
Date: 2025-02-25 Tue 12:32
Created: 2025-03-02 Sun 20:19