Building an effective Image Component
An image component encapsulates performance best practices and provides an out-of-the-box solution to optimize images.
Images are a common source of performance bottlenecks for web applications and a key focus area for optimization. Unoptimized images contribute to page bloat and currently account for over 70% of the total page weight in bytes at the 90th percentile. Multiple ways to optimize images call for an intelligent "image component" with performance solutions baked in as a default.
The Aurora team worked with Next.js to build one such component. The goal was to create an optimized image template that web developers could further customize. The component serves as a good model and sets a standard for building image components in other frameworks, content management systems (CMS), and tech-stacks. We have collaborated on a similar component for Nuxt.js, and we are working with Angular on image optimization in future versions. This post discusses how we designed the Next.js Image component and the lessons we learned along the way.

Image optimization issues and opportunities #
Images not only affect performance, but also business. The number of images on a page was the second greatest predictor of conversions of users visiting websites. Sessions in which users converted had 38% fewer images than sessions where they did not convert. Lighthouse lists multiple opportunities to optimize images and improve web vitals as part of its best practices audit. Some of the common areas where images can affect core web vitals, and user experience are as follows.
Unsized images hurt CLS #
Images served without their size specified can cause layout instability and contribute to a high Cumulative Layout Shift (CLS). Setting the width
and height
attributes on img elements can help to prevent layout shifts. For example:
<img src="flower.jpg" width="360" height="240">
The width and height should be set such that the aspect ratio of the rendered image is close to its natural aspect ratio. A significant difference in the aspect ratio can result in the image looking distorted. A relatively new property that allows you to specify aspect-ratio in CSS can help to size images responsively while preventing CLS.
Large images can hurt LCP #
The larger the file size of an image, the longer it will take to download. A large image could be the "hero" image for the page or the most significant element in the viewport responsible for triggering the Largest Contentful Paint (LCP). An image that is part of the critical content and takes a long time to download will delay the LCP.
In many cases, developers can reduce image sizes through better compression and the use of responsive images. The srcset
and sizes
attributes of the <img>
element help to provide image files with different sizes. The browser can then choose the right one depending on the screen size and resolution.
Poor image compression can hurt LCP #
Modern image formats like AVIF or WebP can provide better compression than commonly used JPEG and PNG formats. Better compression reduces the file size by 25% to 50% in some cases for the same quality of the image. This reduction leads to faster downloads with less data consumption. The app should serve modern image formats to browsers that support these formats.
Loading unnecessary images hurts LCP #
Images below the fold or not in the viewport are not displayed to the user when the page is loaded. They can be deferred so that they do not contribute to the LCP and delay it. Lazy-loading can be used to load such images later as the user scrolls towards them.
Optimization challenges #
Teams can evaluate the performance cost due to the issues listed previously and implement best practice solutions to overcome them. However, this often does not happen in practice, and inefficient images continue to slow down the web. Possible reasons for this include:
- Priorities: Web developers usually tend to focus on code, JavaScript, and data optimization. As such, they may not be aware of issues with images or how to optimize them. Images created by designers or uploaded by users may not be high in the list of priorities.
- Out-of-the-box solution: Even if developers are aware of the nuances of image optimization, the absence of an all-in-one out-of-the-box solution for their framework or tech-stack may be a deterrent.
- Dynamic images: In addition to static images that are part of the application, dynamic images are uploaded by users or sourced from external databases or CMS's. It may be challenging to define the size of such images where the source of the image is dynamic.
- Markup overload: Solutions for including the image size or
srcset
for different sizes require additional markup for every image, which can be tedious. Thesrcset
attribute was introduced in 2014 but is used by only 26.5% of the websites today. When usingsrcset
, developers have to create images in various sizes. Tools such as just-gimme-an-img can help but have to be used manually for every image. - Browser support: Modern image formats like AVIF and WebP create smaller image files but need special handling on browsers that don't support them. Developers have to use strategies like content negotiation or the
<picture
> element so that images are served to all browsers. - Lazy loading complications: There are multiple techniques and libraries available to implement lazy-loading for below-the-fold images. Picking the best one can be a challenge. Developers may also not know the best distance from the "fold" to load deferred images. Different viewport sizes on devices can further complicate this.
- Changing landscape: As browsers start supporting new HTML or CSS features to enhance performance, it may be difficult for developers to evaluate each of them. For example, Chrome is introducing the Priority Hints feature as an Origin Trial. It can be used to boost the priority of specific images on the page. Overall, developers would find it easier if such enhancements were evaluated and implemented at the component level.
Image component as a solution #
The opportunities available to optimize images and the challenges in implementing them individually for every application led us to the idea of an image component. An image component can encapsulate and enforce best practices. By replacing the <img>
element with an image component, developers can better address their image performance woes.
Over the last year, we have worked with the Next.js framework to design and implement their Image component. It can be used as a drop-in replacement for the existing <img>
elements in Next.js apps as follows.
// Before with <img> element:
function Logo() {
return <img src="/logo.jpg" alt="logo" height="200" width="100" />
}
// After with image component:
import Image from 'next/image'
function Logo() {
return <Image src="/logo.jpg" alt="logo" height="200" width="100" />
}
The component tries to address image-related problems generically through a rich set of features and principles. It also includes options that allow developers to customize it for various image requirements.
Protection from layout shifts #
As discussed previously, unsized images cause layout shifts and contribute to CLS. When using the Next.js Image component, developers must provide an image size using the width
and height
attributes to prevent any layout shifts. If the size is unknown, developers must specify layout=fill
to serve an unsized image that sits inside a sized container. Alternatively you can use static image imports to retrieve the size of the actual image on the hard drive at build time and include it in the image.
// Image component with width and height specified
<Image src="/logo.jpg" alt="logo" height="200" width="100" />
// Image component with layout specified
<Image src="/hero.jpg" layout="fill" objectFit="cover" alt="hero" />
// Image component with image import
import Image from 'next/image'
import logo from './logo.png'
function Logo() {
return <Image src={logo} alt="logo" />
}
Since developers cannot use the Image component unsized, the design ensures that they will take the time to consider image sizing and prevent layout shifts.
Facilitate responsiveness #
To make images responsive across devices, developers must set the srcset
and sizes
attributes in the <img>
element. We wanted to reduce this effort with the Image component. We designed the Next.js Image component to set the attribute values only once per application. We apply them to all instances of the Image component based on the layout mode. We came up with a three-part solution:
deviceSizes
property: This property can be used to configure breakpoints one-time based on the devices common to the application user base. The default values for breakpoints are included in the config file.imageSizes
property: This is also a configurable property used to get the image sizes corresponding to device size breakpoints.layout
attribute in each image: This is used to indicate how to use thedeviceSizes
andimageSizes
properties for each image. The supported values for layout mode arefixed
,fill
,intrinsic
andresponsive
When an image is requested with layout modes responsive or fill, Next.js identifies the image to be served based on the size of the device requesting the page and sets the srcset
and sizes
in the image appropriately.
The following comparison shows how the layout mode can be used to control the size of the image on different screens. We have used a demo image shared in the Next.js docs, viewed on a phone and a standard laptop.
Laptop screen | Phone screen | |||
---|---|---|---|---|
Layout = Intrinsic: Scales down to fit the container's width on smaller viewports. Does not scale up beyond the image's intrinsic size on a larger viewport. Container width is at 100% | ||||
![]() | ![]() | |||
Layout = Fixed: Image is not responsive. Width and height are fixed similar to ` | ||||
![]() | ![]() | |||
Layout = Responsive: Scale down or scale up depending on the width of the container on different viewports, maintaining aspect ratio. | ||||
![]() | ![]() | |||
Layout = Fill: Width and height stretched to fill the parent container. (Parent ` ` width is set to 300*500 in this example) ![]() ![]() |