Next.js 11 Image Component in Practice

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.

Setup

To keep things realistic for this example, I've created a rough copy of the netflix landing page on mobile.

netflix sample

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.

Native img - our baseline

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"
/>

performance scores. 100/100 first-contentful-paint, 92/100 largest contentful paint, 100/100 first meainingful paint, 100/100 speed index, 100/100 preload lcp image

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.

Next Image

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!

performance scores. 99/100 first-contentful-paint, 97/100 largest contentful paint, 100/100 first meainingful paint, 100/100 speed index, 87/100 preload lcp image

With the help of automatically inserted srcset, the image we are loading is significantly smaller and our LCP metric improves drastically.

Next Image with Priority

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.

performance scores. 99/100 first-contentful-paint, 95/100 largest contentful paint, 100/100 first meainingful paint, 100/100 speed index, 100/100 preload lcp image

Our change is clearly visible in the Preload LCP Image score.

Next Image with Blur Placeholder

<Img
  src={heroImg}
  layout="fill"
  objectFit="cover"
  alt="Hero Image"
  priority={true}
  placeholder="blur"
/>

performance scores. 100/100 first-contentful-paint, 94/100 largest contentful paint, 100/100 first meainingful paint, 100/100 speed index, 100/100 preload lcp image

By inserting a blurred placeholder while we're loading the actual image, we can improve the First Contentful Paint metric.

Overall Performance Scores & Conclusions

overall performance scores. img tag: 98, Image: 99, Image Preload: 99, Img Preload Blur: 98

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.