The rapid advancement of the web brings so many abstractions to the table to let developers quickly build and ship projects. However, this simplicity also comes with the problem of developers not understanding what a significant piece of their code even does.

Most modern web frameworks and libraries include an Image component that automatically optimizes an image for all clients (browsers) and all screen sizes, but how exactly does it do this?

Let’s build our own Image component from scratch by implementing all the core optimization features, starting with an img tag.

interface Props {
  src: string;
  alt: string;
}

function Image({ src, alt }: Props) {
  return <img src={src} alt={alt} />;
}

The alt attribute is often missed by developers but is quite helpful in boosting SEO, so it is mandated. It is also essential for accessibility, as people with vision impairment rely on it if they are unable to view the image or can’t make out parts of it. If there is no alt text, it can just be left as an empty string literal. The role attribute can also be set to presentation if there is no alt text to display.

Now the img tag comes with a super useful loading attribute that lets us only load the images when they are actually displayed by setting it to lazy, saving both time and bandwidth while loading the page.

<img loading="lazy" ... />

Another loading property offered by the img tag is the fetchPriority, which hints to the browser about the priority of loading this image relative to the other images. This should generally be set to high on the large contentful paint images.

We can abstract this by just exposing a boolean attribute called priority.

interface Props {
  src: string;
  alt: string;
  priority: boolean;
}

function Image({ src, alt, priority }: Props) {
  return (
    <img
      src={src}
      fetchpriority={priority ? "high" : "auto"}
      loading={priority ? "eager" : "lazy"}
      alt={alt}
    />
  );
}

Lazy loading should be disabled for images with high-priority loading.

We can also go ahead and set the decoding property to async to force the browser to asynchronously load the image, which will reduce any delay in presenting other content.

<img decoding="async" ... />

Sizing

It is important to specify the image size beforehand to prevent any Cumulative Layout Shifts , which can be extremely annoying UX-wise.

This can either be done through the height and width attribute or by the aspect-ratio CSS style. We’ll use the former here.

interface Props {
  height: number;
  width: number;
  ...
}

function Image({height, width, ...}: Props) {
  return (
    <img
      height={height}
      width={width}
      ...
    />
  );
}

A few web frameworks also include a built-in optimization API, which serves the same image in different sizes and qualities. By using a loader function on the front end, the different images can be loaded based on the screen size and resolution of the device.

Now let’s render an image with the component we’ve built so far.

<Image
  src="https://images.unsplash.com/photo-1611003228941-98852ba62227"
  alt="A cute dog"
  height={297}
  width={396}
  priority
/>

The above JSX results in the following html element.

<img
  src="https://images.unsplash.com/photo-1611003228941-98852ba62227"
  fetchpriority="high"
  loading="eager"
  alt="A cute dog"
  height="297"
  width="396"
  decoding="async"
/>
A cute dog

Thanks for reading!

© 2024 Shiv. All rights reserved.

Decoded: Image Optimization on the Web

Last updated on