Performance in real life

Auteur(s) de l'article

Following the article on “Understanding Core Web Vitals”, I wanted to write about how we are handling this aspect on a project currently being developed at Antistatique. For confidentiality purposes, I won’t name the client, but let’s call this project “A”.
As stated before, performance is a key-factor in how successful a website is. One of the most common ways of measuring it is by using “Lighthouse”. For project “A”, my objective was to get every page to be in the “green” zone (a performance score equal or above to 90) - on desktop and mobile.
It is easier to get good performances when this objective is set from the beginning of the project. The optimization process can be quite heavy and implies a lot of refactoring if it is thought about after most of the development has been done.

A few words about project “A”

The client for which we are developing the project “A” is based in Switzerland and mostly concerned with a Swiss audience. It is quite a big project with lots of different contents, images and videos, as well as some animations.
The stack we chose in conjunction with the client was the Drupal CMS. We also developed a styleguide for our internal use, which contains the components integrated afterwards in the twig templates. The styleguide was developed in React thanks to our frontend tool Storybox and serves the build assets to the main Drupal theme via Github package.

Handling images

One of the main recurring issues we have across all our projects is how best to handle the images. In this case, knowing that there would be quite a lot of them, it was even more important to think of this from the get-go. The usual way to handle this was to create image styles in Drupal and then render them as such:
<picture>
  <source media="(max-width: 576px)" srcset="{{ bamboo_render_image_style(uri, 'slider_1x') }} 1x, {{ bamboo_render_image_style(uri, 'slider_2x') }} 2x">
  <img src="{{ bamboo_render_image_style(uri, 'slider_1x') }}" class="{{ classes|join(' ') }}" alt="{{ alt }}">
</picture>
Still, even if the solution above does work, we had room for improvement. We started by replacing the media attribute with sizes. It basically specifies different widths for the image according to media queries. Then the browser will display the correct image depending on the window size.
We kept the srcset attribute on the <source> tag element, but instead of playing with the pixel density, we gave a lot more different images with different width. This way, the browser decides which is the best image according to a lot of parameters, such as network connection, window size, device, etc.
Finally, we took advantage of the new image formats and added a second <source> tag. The first one contains the jpeg images in different widths. The second one is the exact same, except it serves webp assets. This way, the browser can display the webp assets whenever the device being used allows it.
The most complicated part in all of this was to ensure we had all the image styles needed. Most of the images on project “A” were on a 16:9 or 2.39:1 ratios which simplified the creation of image styles. The real challenge was for the “free” ratio because we could not crop them to a specific ratio, only allow them to grow or shrink to specific heights. We were still able to create image styles by using a scale effect on the width (the height being known) instead.
Also, in order to generate the right sizes attributes, I've used this tool to nudge me in the right direction. Finally, we also added the lazy loading attribute whenever it was needed.
<div class="{{ ratio == '16:9' ? 'aspect-w-16 aspect-h-9' : '' }}{{ ratio == '2.39:1' ? 'aspect-w-2.39 aspect-h-1' : '' }} {{ classes ?? '' }}">
  <picture>
    <source
      srcSet="{{ srcSets.webpSrcSets }}"
      type="image/webp"
      sizes="{{ sizes }}"
    >
    <source
      srcSet="{{ srcSets.srcSets }}"
      sizes="{{ sizes }}"
    >
    <img
      alt="{{ img.alt.value }}"
      src="{{ file_url(img.uri.value) }}"
      loading="{{ loading == '' ? undefined : loading }}"
      class="object-cover object-center w-full h-full"
      width="{{ width }}"
      height="{{ height }}"
    />
  </picture>
</div>

Video

While I have focused less time on the optimization of videos compared to images, a few word can still be said.
For videos which are directly hosted on the website, the use of the <video> tag is perfect, knowing it has a lot of the same attributes as the <picture> tag: you can set multiple sources or provide the preload attribute for example. The important think here is to compress the video to ensure the smallest size.
But a lot of the time, we tend to use iframe to embed the videos, which are not so great performance-wise. A tool that can come in handy is Lite Youtube Embed, which renders the element approximately 224× faster according to its creator. I haven't used it myself, but it has been recommended by one of my coworker.

Managing resources

CSS

For quite a while now, we have been using TailwindCSS in our projects. One of the very useful features is the CSS purge. This way we are sure to have only the CSS we actually use in our project, which ensures the smaller file size possible. The CSS file is then loaded in one bulk and cached.

JavaScript

Regarding JavaScript: when working on a website, we usually start by building the components in a styleguide. This is also the moment when we add the necessary JavaScript. Working this way means we can chunk the JavaScript in different modules, each with a specific functionality. Then it is only a matter of using the JavaScript where it is needed, instead of having one big file loaded at once.
This process allows us to use the defer attribute, when we did not need to have the JavaScript loaded right away. For example, some JavaScript is necessary on every page (in our case, mostly what was related to the menu), while the JavaScript related to filters was only needed on some very specific pages.
We also tried to avoid the use of external JavaScript library whenever possible, simply to prevent the size of the bundle to increase.
An example was on a page which was designed with a Mapbox map. I first started using the Mapbox library, but soon realized that it  was extremely heavy (about ~671KB) and had a significant impact on the Lighthouse score. Upon reflection with the designer, we considered that there was no real added benefits to have such a map on the site given its limited use. So instead of using the full JavaScript library, we went for a static image map also by Mapbox, sufficient to answer the need for this page.
Obviously, this is not always a choice that can or should be made, and this case is very specific, but it is always good to reflect on the library choices and if they are relevant to our needs.

Animations

This project also had two big animations on two different pages. It was easier and faster to develop those entirely in React with the help of the FramerMotion library and to implement them as widgets in the code. But to do so meant to load both the React and React-dom libraries, besides the actual animation code. This can prove quite heavy in terms of performance. But those two animations were important and we decided it was worth it.
To minimize the decrease in performance, a few things have been done: For one of the animation, we decided to forsake the animation on mobile and replace it with some HTML/CSS code. This way, we didn’t need to load the React lib on mobile, gaining consequently in the performance area.
The second thing done was to prevent layout shifting due to those widgets. As they are dynamically inserted elements, if the space isn’t reserved, the layout would shift as soon as it is loaded.

So, in the end…

I am quite happy with the performance overall. Obviously, the homepage which contains one of the React widget is slightly impacted by it (~88 for the Lighthouse score during the last check).
Every other pages seems to be in the green, both desktop and mobile, at the time of writing this article. Of course, we need to see with real world data before assuming everything is great, but it is at least a good start.
To keep track of the Lighthouse performance during development, we used a Github action which would run everytime we pushed something on the dev branch (staging), testing some specific pages. For the styleguide, we kept an eye on the bundle sizes during the development phase.
The reflections that started on this project generated some outcomes also in other projects. The image optimisation was implemented in other projects, even some with different stacks. For a headless website, we also made use of the dynamic import from NextJS, drastically reducing the loading time for the pages. In Drupal, a tool to generate image styles more easily was also developed by some of my coworkers.
This is only a few steps in the right direction. There is more to be done, also depending on the situations, projects and stacks chosen. But at least, we are talking and reflecting on what we can do better.