The Task
Todays task is going to be about putting the theory we learnt in the previous session into something more practical by implementing those best practices to common design patterns on the web. Our exercise page is a festival page which has around five components on there that each have a unique problem that needs to be solved. Again, we are going to be using the unsplash api to make our changes to this web page.
Hero
A hero component can be a very challenging component to implement web performance changes because you want to find that right balance between a really nice image that people first see but also, you want to load that image really fast as that is the entry view.
Responsive images
First thing we want to do is make the image load different image sizes at different breakpoints so that we can load a larger image in when the screen is larger. We probably want to do this for three different breakpoints which is mobile, small tablet, tablet and desktop. In _hero.njk we need to change the image tag to use a picture element:
<picture class="absolute top-0 left-0 right-0 bottom-0 z-10">
<source srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1600&h=900&fit=crop" media="(min-width: 767px)" />
<source srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1024&h=640&fit=crop" media="(min-width: 767px)" />
<source srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=767&h=640&fit=crop" media="(min-width: 480px)" />
<img
src="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=480&h=800&fit=crop"
alt="DJ playing at a festival"
class="w-full object-cover h-full"
/>
</picture>
Image formats
Now we have smaller images loading in, we can now take it to the next step and start loading in different image formats.
<picture class="absolute top-0 left-0 right-0 bottom-0 z-10">
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=1600&h=900&fit=crop&fm=avif"
type="image/avif"
media="(min-width: 1024px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=1600&h=900&fit=crop&fm=webp"
type="image/webp"
media="(min-width: 1024px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=1600&h=900&fit=crop&fm=jpg"
type="image/jpg"
media="(min-width: 1024px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=1024&h=640&fit=crop&fm=avif"
type="image/avif"
media="(min-width: 767px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=1024&h=640&fit=crop&fm=webp"
type="image/webp"
media="(min-width: 767px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=1024&h=640&fit=crop&fm=jpg"
type="image/jpg"
media="(min-width: 767px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=767&h=640&fit=crop&fm=avif"
type="image/avif"
media="(min-width: 480px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=767&h=640&fit=crop&fm=webp"
type="image/webp"
media="(min-width: 480px)"
/>
<source
srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=767&h=640&fit=crop&fm=jpg"
type="image/jpg"
media="(min-width: 480px)"
/>
<source srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=480&h=800&fit=crop&fm=avif" type="image/avif" />
<source srcset="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=480&h=800&fit=crop&fm=webp" type="image/webp" />
<img
src="https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?q=70&w=480&h=800&fit=crop"
alt="DJ playing at a festival"
class="w-full object-cover h-full"
/>
</picture>
Perceived loading
If we know the image might take a while to load we can use a background colour or an image behind the image to make the text more visible to the user. This is another way to make the perceived loading of the image a bit smoother.
<picture
class="absolute top-0 left-0 right-0 bottom-0 z-10"
style="background: url('https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?blur=1550&q=70&fm=jpg&w=12&fit=clip')"
>
...
</picture>
Lazy-loading
Now, lazy-loading is a good way to making images load faster however, when an image is in the top of the viewport we want that image to load as fast as possible, not delayed. So we can skip this step for now.
Fetch priority
Previous section we stated that lazy-loading a hero image can actually make the load of the image slower. So I guess the next thing we can do is look into another way to speed the load up. fetchPriority is a way to tell the browser that we want to treat this image as a high priority image to load in the network. This will be loaded in the same priority as javascript and css files.
Image and text block
Image and text block is another common pattern you see in most web pages where it is pretty much a static image that stacks above the text on mobile and then appears on either side on desktop. The work we are going to be doing will be in the _block.njk file.
Responsive images
Not much is needed to be done in this space because the image is roughly the same size in every breakpoint. What we can do here is just set a standard width, height and quality to both images.
<picture class="lg:w-1/2 flex-1">
<img
src="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70"
alt="image of people having a good time at a festival"
class="block w-full"
/>
</picture>
Image formats
Now we have smaller images loading in, we can now take it to the next step and start loading in different image formats.
<picture class="lg:w-1/2 flex-1">
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=avif" type="image/avif" />
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=webp" type="image/webp" />
<img
src="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70"
alt="image of people having a good time at a festival"
class="block w-full"
/>
</picture>
Lazy-loading
This is a prime example to where we can apply lazy-loading because it is further down the page and we can just simply add loading="lazy" to the img element.
<picture class="lg:w-1/2 flex-1">
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=avif" type="image/avif" />
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=webp" type="image/webp" />
<img
src="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70"
alt="image of people having a good time at a festival"
class="block w-full"
loading="lazy"
/>
</picture>
Perceived loading
If we know the image might take a while to load we can use a background colour or an image behind the image to make the text more visible to the user. This is another way to make the perceived loading of the image a bit smoother.
<picture
class="lg:w-1/2 flex-1"
style="background: url('https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?blur=1550&q=70&fm=jpg&w=12&fit=clip')"
>
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=avif" type="image/avif" />
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=webp" type="image/webp" />
<img
src="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70"
alt="image of people having a good time at a festival"
class="block w-full"
loading="lazy"
/>
</picture>
Layout shifting
One common thing that we should do one every image is define an aspect ratio so that the page does not jump around when it is loading in the images. Add the block-image-aspect-ratio to each of the image/picture elements in the block njk file and then in 04.css we want to create that selector.
<picture
class="lg:w-1/2 flex-1 block-image-aspect-ratio"
style="background: url('https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?blur=1550&q=70&fm=jpg&w=12&fit=clip')"
>
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=avif" type="image/avif" />
<source srcset="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70&fm=webp" type="image/webp" />
<img
src="https://images.unsplash.com/photo-1574155376612-bfa4ed8aabfd?w=450&h=300&q=70"
alt="image of people having a good time at a festival"
class="block w-full block-image-aspect-ratio"
loading="lazy"
/>
</picture>
.block-image-aspect-ratio {
aspect-ratio: 450 / 300;
}
Carousel
Carousels are very similar to image and text blocks however, what problem this component has is that it has images that are hidden off screen that still load in the page.
Lazy-loading
So in the past, this would have been really difficult to prevent images from loading in the page based on their horizontal scroll position. Some JS libraries have solved this but a quick win for us can simply be just adding loading="lazy".
Apply this attribute to each slide:
<picture id="slide-one">
<img src="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439" alt="Festival one" class="block" loading="lazy" />
</picture>
Responsive images
The next thing we want to do now is make these images responsive so on mobile, when a user is swiping through the images the images feel almost instant when loading in.
<picture id="slide-one">
<source srcset="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=450&h=300&q=70" media="(min-width: 480px)" />
<img src="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=300&h=200&q=70" alt="Festival one" class="block" loading="lazy" />
</picture>
Image formats
To make this even faster, let's now look into optimising the image format.
<picture id="slide-one">
<source
srcset="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=450&h=300&q=70&fm=avif"
media="(min-width: 480px)"
type="image/avif"
/>
<source
srcset="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=450&h=300&q=70&fm=webp"
media="(min-width: 480px)"
type="image/webp"
/>
<source srcset="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=450&h=300&q=70&fm=jpg" media="(min-width: 480px)" type="image/jpg" />
<source srcset="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=450&h=300&q=70&fm=avif" type="image/avif" />
<source srcset="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=450&h=300&q=70&fm=webp" type="image/webp" />
<img src="https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?w=300&h=200&q=70" alt="Festival one" class="block" loading="lazy" />
</picture>
YouTube videos
Youtube videos can be an expensive download for a user especially when they never interacted with it. So this area is about how we can mitigate that download for them but also speed up our page load while doing so.
YouTube Image Pattern
If you have free reign on changing the markup of a YouTube component then I recommend a pattern where you add the YouTube thumbnail and mimic the youtube play button infront of it. What this will do is download an image version of the iframe and then on click of the button we can load the iframe in and set it to autoplay.
Setting up the markup
So instead of just planting the iframe right in the page, what we can do is setup our markup for the thumbnail and button and remove the iframe.
<div class="relative w-full" data-youtube-component>
<img
src="https://i.ytimg.com/vi/q7SCZb4zacU/maxresdefault.jpg"
class="block w-full object-cover h-full"
alt="Image of Wh0 playing a DJ set from YouTube."
/>
<button
type="button"
aria-label="Play Youtube Video"
class="absolute top-0 left-0 right-0 bottom-0 z-10 flex items-center justify-center"
data-youtube-button="q7SCZb4zacU"
>
<svg viewBox="0 0 48 48" width="90px" height="90px">
<linearGradient id="PgB_UHa29h0TpFV_moJI9a" x1="9.816" x2="41.246" y1="9.871" y2="41.301" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f44f5a" />
<stop offset=".443" stop-color="#ee3d4a" />
<stop offset="1" stop-color="#e52030" />
</linearGradient>
<path
fill="url(#PgB_UHa29h0TpFV_moJI9a)"
d="M45.012,34.56c-0.439,2.24-2.304,3.947-4.608,4.267C36.783,39.36,30.748,40,23.945,40 c-6.693,0-12.728-0.64-16.459-1.173c-2.304-0.32-4.17-2.027-4.608-4.267C2.439,32.107,2,28.48,2,24s0.439-8.107,0.878-10.56 c0.439-2.24,2.304-3.947,4.608-4.267C11.107,8.64,17.142,8,23.945,8s12.728,0.64,16.459,1.173c2.304,0.32,4.17,2.027,4.608,4.267 C45.451,15.893,46,19.52,46,24C45.89,28.48,45.451,32.107,45.012,34.56z"
/>
<path
d="M32.352,22.44l-11.436-7.624c-0.577-0.385-1.314-0.421-1.925-0.093C18.38,15.05,18,15.683,18,16.376 v15.248c0,0.693,0.38,1.327,0.991,1.654c0.278,0.149,0.581,0.222,0.884,0.222c0.364,0,0.726-0.106,1.04-0.315l11.436-7.624 c0.523-0.349,0.835-0.932,0.835-1.56C33.187,23.372,32.874,22.789,32.352,22.44z"
opacity=".05"
/>
<path
d="M20.681,15.237l10.79,7.194c0.689,0.495,1.153,0.938,1.153,1.513c0,0.575-0.224,0.976-0.715,1.334 c-0.371,0.27-11.045,7.364-11.045,7.364c-0.901,0.604-2.364,0.476-2.364-1.499V16.744C18.5,14.739,20.084,14.839,20.681,15.237z"
opacity=".07"
/>
<path
fill="#fff"
d="M19,31.568V16.433c0-0.743,0.828-1.187,1.447-0.774l11.352,7.568c0.553,0.368,0.553,1.18,0,1.549 l-11.352,7.568C19.828,32.755,19,32.312,19,31.568z"
/>
</svg>
</button>
</div>
Writing up the JavaScript
We now just need to setup the JavaScript for this YouTube component to work properly.
/assets/js/global/youtube.js
(function () {
'use strict';
const youtubeElements = [...document.querySelectorAll('[data-youtube-component]')];
const init = (element) => {
const button = element.querySelector('[data-youtube-button]');
const image = element.querySelector('[data-youtube-image]');
button.addEventListener('click', () => {
const iframe = document.createElement('iframe');
iframe.width = '100%';
iframe.height = '480';
iframe.src = `https://www.youtube.com/embed/${button.getAttribute('data-youtube-button')}`;
iframe.title = 'YouTube video player';
iframe.frameborder = '0';
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture;';
iframe.allowfullscreen = 'true';
iframe.classList.add('youtube-image');
element.appendChild(iframe);
element.removeChild(image);
});
};
youtubeElements.forEach(init);
})();
and then we need to add this script in so in our _scripts.njk file we just add:
<script src="/assets/js/global/youtube.js" defer type="text/javascript"></script>
Let's see what we have got.
Layout shifting
Same as the image blocks, we want to define an aspect ratio so that the page does not jump around when it is loading in the youtube image. Add the youtube-image to the data-youtube-image element in the block njk file and then in 04.css we want to create that selector.
.youtube-image {
aspect-ratio: 712 / 534;
}
Lazy-loading
Simple one for this, what we can do here is add the loading=lazy to the youtube thumbnail image.
<img
src="https://i.ytimg.com/vi/q7SCZb4zacU/maxresdefault.jpg"
class="block w-full object-cover h-full"
alt="Image of Wh0 playing a DJ set from YouTube."
loading="loading"
/>
Perceived loading
The youtube thumbnail doesn't have a blurred version of itself so we can just set a generic background colour for this image.
<div class="lg:w-1/2 flex-1 bg-slate-300" data-youtube-component>...</div>
If you have less control over the iframe...
You can also lazy load iframes like you do with images.
<iframe
width="100%"
height="480"
src="https://www.youtube.com/embed/q7SCZb4zacU"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
loading="lazy"
>
</iframe>