Skip to main content

Nextjs & Storyblok image optimization. Part 1

This comprehensive two-part article explores the critical role of image optimization in web development. In the first part, it underscores the profound impact of optimized images on user experience and SEO rankings, addressing issues like slow loading, bandwidth consumption, and the importance of faster websites in search results.

This is the first part of the article. The second one can be found here

In this article we'll cover:

Importance of Image Optimization in Web Development

Image optimization is crucial in web development. It impacts user experience and SEO ranking:

  • large, unoptimized images can significantly slow down webpage loading;

  • slow pages can negatively affect user experience;

  • unoptimized images consume more traffic, this affects you and your users: large images - more CDN traffic - higher costs for you, mobile devices use an expensive mobile network more traffic - higher costs for users;

  • faster websites tend to rank higher in search results.

    The HTML simplest img tag is sufficient for simple use-cases, but not for a large amount of images or big size images. In this article, we will touch on the topic of image optimization with Nextjs and Storyblok. The main approach here is to control the size of the loaded images and delay loading an image until it is actually needed.

Example of Image Optimization Process Using Next.js Image Component

The simplest way to optimize images in Nextjs is to use built in Image from next/image. To enable this we need to add domain in next.config.js:

const nextConfig = {
images: {
domains: ["a.storyblok.com"],
},
};

In our example it’s enough, but you can also limit yourself to a certain path:

module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'a.storyblok.com',
port: '',
pathname: '/f/185240/**',
},
],
},
}

Let’s put an image on a page:

import Image from "next/image";

export default function Page() {
const width = 400;
const aspectRatio = 5266 / 3403;
const height = width / aspectRatio;

return (
<div>
<Image
src="https://a.storyblok.com/f/185240/5266x3403/99cbc784c3/pexels-eberhard-grossgasteiger-443446.jpg"
alt="landscape"
width={width}
height={height}
/>
</div>
);
}

This image was uploaded to Storyblok by a content manager. It has JPG format and 4.57 MB size.

Image Optimization Results

We can inspect the image in a browser and see something like this:

<img alt="landscape" loading="lazy" width="400" height="258.4884162552222" decoding="async" data-nimg="1"
style="color:transparent"
srcset="/_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=640&amp;q=75 1x, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=828&amp;q=75 2x"
src="/_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=828&amp;q=75">

NextJS added several attributes for optimization:

  • loading: “lazy” – load the image only when needed (when appears in the viewport)
  • decoding: “async” – let other content to be displayed and asynchronously decode the image
  • srcset="imageurl640 1x, imageurl828 2x" – this one provides different URLs for devices with different pixel densities, e.g. Retina displays have a higher pixel density and require higher quality images.

Note: By default we use loading: “lazy” but if the image is "Largest Contentful Paint" (LCP), we must pass the priority: true attribute to specify explicitly that the image should be loaded with the highest priority. With this attribute the browser will preload the image

The original image size was 4.57 MB, JPG format. but after optimization it became 116kB and WebP. This is the result of the image optimisation process that NextJS performs on a build step. It fetch images during that step, resizes them and put to a dist folder. So the app will consume them from the same place when the app is hosted but not from the CMS

If you use an image with no dimensions it will result in CLS (Cumulative Layout Shift) when you have content below the image -- in most cases you will have CLS. For example:

import Image from 'next/image'
export default function Page() {
return (
<main>
<Image
src="https://a.storyblok.com/f/185240/5266x3403/99cbc784c3/pexels-eberhard-grossgasteiger-443446.jpg"
alt="landscape"
// here should be dimensions to avoid CLS
/>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
</main>
)
}

The text below the image will be shifted after the image has fully loaded

There are two options to solve this. We can display the image with or without a wrapper. In the example above the image needs a height and a width. Another way is to reserve a space and fill it with an image. Let’s add three images the following way:

import Image from "next/image";
export default function Page() {
const src = "https://a.storyblok.com/f/185240/5266x3403/99cbc784c3/pexels-eberhard-grossgasteiger-443446.jpg"

return (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
gap: "48px",
flexDirection: "column",
}}
>
<div style={{ position: "relative", width: "100%", height: "200px" }}>
{/* the image will be distorted, the proportions are not respected */}
<Image
src={src}
alt="landscape"
fill
/>
</div>
<div style={{ position: "relative", width: "100%", height: "200px" }}>
{/* the image will be cropped to show part of the original image (center part in this case) to fit in the available space */}
<Image
src={src}
alt="landscape"
fill
objectFit="cover"
// objectPosition="top" or objectPosition="bottom" to show different part of the image
/>
</div>
<div style={{ position: "relative", width: "100%", height: "200px" }}>
{/* Best case? fit the image into the available space, taking into account the aspect ratio */}
<Image
src={src}
alt="landscape"
fill
objectFit="contain"
/>
</div>
</div>
);
}

In a browser devtools we will see the first image like this:

<img alt="landscape" loading="lazy" decoding="async" data-nimg="fill"
style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:contain;color:transparent"
sizes="100vw"
srcset="/_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=640&amp;q=75 640w, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=750&amp;q=75 750w, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=828&amp;q=75 828w, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=1080&amp;q=75 1080w, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=1200&amp;q=75 1200w, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=1920&amp;q=75 1920w, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=2048&amp;q=75 2048w, /_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=3840&amp;q=75 3840w"
src="/_next/image?url=https%3A%2F%2Fa.storyblok.com%2Ff%2F185240%2F5266x3403%2F99cbc784c3%2Fpexels-eberhard-grossgasteiger-443446.jpg&amp;w=3840&amp;q=75">

We can notice that in srcset attribute each size has individual url, also a new attribute added sizes=”100vw” – this one provide a rule how to pick URL for an image from srcset based on size

Here are some key points regarding sizes attribute:

  • the sizes property specifies media conditions and image sizes for responsive image loading;
  • it works in tandem with the srcset property to help the browser choose the best image for the device;
  • sizes is a string with comma-separated pairs of media conditions and image sizes (in width followed by w). For example, (max-width: 640px) 100vw, 50vw means that for screens up to 640px wide, the image will be 100% viewport width, and for wider screens, it'll be 50% viewport width;
  • you can also use pixel values in sizes for specifying image sizes.

The picture below shows three different ways of rendering the same image:

  1. objectFit isn’t specified. Image stretched to reserved space. As we don't know the actual size of the image, the image will be distorted mostly in all cases.

  2. objectFit="cover" We show a piece of the image in the available slot. It might work if we knew which part to focus on. And also the image may look cropped.

  3. objectFit="contain". We scale down the image to fit the available space considering the aspect ratio.

The last case seems acceptable, but in most cases we want the image to be displayed at full width and full height, respectively -- it seems that we need to know the dimensions of the image.

In this article, we highlighted the importance of image optimization in web development influencing user experience and SEO rankings. We have looked at how to optimize images from Headless CMS with NextJS means.

In the second part of the article, we'll take a look at the Storyblok image service and look at an approach to getting image sizes.

WRITTEN BY

Sergey Labuts

Sergey Labuts

Senior Engineer at FocusReactive