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"
/>
Thanks for reading!