In Next.js 11 the image tag is updated and gets additional features like the ability to import images and to use blur placeholders. Let's examine the effects of the image tag and how it helps us to improve performance.
To keep things realistic for this example, I've created a rough copy of the netflix landing page on mobile.
The background image on this site can be seen as a typical example for a hero image. A type of image we commonly find on different kinds of websites and of special interest to us. Hero images can play a huge role in a webpage's performance for two reasons. First, they are rather large and typically span across the entire browser width or at least a significant fraction of it. This leads to them being rather large in size. In combination with being above the fold and being the relevant element for Largest Contentful Paint (LCP) on our site, they play a huge role.
Let's see how we can optimize the image, user experience and our site's performance with Next's image component. But first, we will see the unoptimized case, with a simple <img> tag.
The performance scores in this article are based on running Google Lighthouse 30 times against each version. Each table gives the average scores for a selection of relevant metrics.
The simplest and most naive way we can add the image would be by simply adding an <img> tag, without any optimizations whatsoever.
<img src="/hero.jpg" alt="Hero Image" style={{ width: '100%', height: '100%', objectFit: 'cover', }} />
The resulting HTML is nearly identical to the JSX we just wrote:
<img src="/hero.jpg" alt="Hero Image" style="width:100%;height:100%;object-fit:cover" />
The naive image tag works okay, apart from a rather low LCP value, which is one of the most relevant performance metrics for a lighthouse score an core web vitals. If we chose an image of a larger size (our original is 2000 pixels wide), this score would significantly decrease.
Since Next11, we don't need to pass an explicit width and height to images anymore if we import an image from our assets
import heroImg from '../public/hero.jpg' // ... ;<Img src={heroImg} layout="fill" objectFit="cover" alt="Hero Image" />
Simply be replacing the native img tag with a Next.js image, we already have a lot of advantages. Next.js automatically creates optimized versions of the image and the resulting html includes a srcset to display smaller images on smaller screens.
With this little change, many of the performance relevant metrics already improve!
With the help of automatically inserted srcset, the image we are loading is significantly smaller and our LCP metric improves drastically.
Typically, our browser will start loading the image, once he encounteres the img tag on our page. However, with the image always being the LCP element, wouldn't it be great if we had a way to speed this up? This is exactly what we can do with the priority attribute, which will place an instruction to load the image in the head of the document.
<Img src={heroImg} layout="fill" alt="Hero Image" priority={true} />
With this change in place, if we we inspect the resulting HTML, we find that we now have a preload instruction in our head tag:
<!DOCTYPE html> <html> <head> <style data-next-hide-fouc="true"> body { display: none; } </style> <noscript data-next-hide-fouc="true"> <style> body { display: block; } </style> </noscript> <meta name="viewport" content="width=device-width" /> <meta charset="utf-8" /> <link rel="preload" as="image" imagesrcset="..." /> </head> </html>
As a result the loading instruction for the image is now at the 246th byte of the document, instead of the 4920 and we communicated to the browser that this image is important and needs to load ASAP.
Our change is clearly visible in the Preload LCP Image score.
<Img src={heroImg} layout="fill" objectFit="cover" alt="Hero Image" priority={true} placeholder="blur" />
By inserting a blurred placeholder while we're loading the actual image, we can improve the First Contentful Paint metric.
Keeping in mind that the small example page is served locally and due to its simplicity and size is already heavily optimized, we can still see the results in the overal performance scores. The simple image tag performed worst, while using the next image tag, improved our performance score. Only in the case of loading an additional blur of the image, did we experience a slight degredation in performance.