Rebuilding our front-end tech stack for the new teleloisirs.fr

yoriiis

Yoriiis

Posted on October 3, 2020

Rebuilding our front-end tech stack for the new teleloisirs.fr

Télé-Loisirs homepage

Getting started

Introduction

Télé-Loisirs is a french TV programs' website with a server-rendered in PHP Symfony. As a front-end engineer, this article covers how we did a progressive rebuild of the website with a new User Interface (UI) and a complete rewrite of the front-end stack. The main goals were to improve page load performances and to facilitate the development of new features with reusable components.

The progressive rebuild

For the UI development, we started with the home page. This page is the main target for our customers and we couldn't change everything in one shot without the risk of losing users. The solution was to rebuild progressively the first page, beginning from the bottom to the top of the page.

This strategy is softer for users, but incredibly more difficult to implement. Indeed, the legacy code needs to cohabit with the new code.

Once all the homepage's components were rebuilt, the clean steps were very important in order to remove all the legacy code and to reorganize the files.


Thinking in components

To build our Design System, all the teams worked together, the UX designers and the front-end and back-end developers. The common goal was to create reusable components all over the site. Throughout the process, we anchored our work around three technical mantras to avoid multiplication of new components:

  • Does the component already exist?
  • Can an existing component require only small updates to fit the needs?
  • Can the design be harmonized?

If any of these questions couldn't be answered positively, we created a new component. It is strict, but was necessary to visually harmonize all our components for our users.

Organize the components

At the beginning we reviewed the mockups of the whole website to identify all the components used by each page. Then we got a list of the components to develop and use-cases.

Components can be specific to a page, shared between a group of pages or shared between all pages. On our site, the groups of pages are related to the sections: program, people, news, amongst others. Each group contains multiple child pages that are free to use either global or group shared components. We added this intermediate level to avoid moving a group shared component to the global level if it wasn't required. The following tree represents our components' structure organization:

# The single page
PAGE_NAME
    # The components of the single page
    COMPONENTS
        COMPONENT_NAME
PAGE_GROUP
    # The child page of the group
    PAGE_NAME
        # The components of the child page
        COMPONENTS
            COMPONENT_NAME
    SHARED
        # The shared components of the group
        COMPONENT_NAME
SHARED
    # The global shared components
    COMPONENT_NAME
Enter fullscreen mode Exit fullscreen mode

This tree structure allows us to organize all components based on where they are used on the website. It makes it easier to manage and use all components either across a page, a group of pages or even the entire website.

Naming the components

For a better understanding of the different components across all teams, we decided to name all of them using a short, simple and descriptive name.

Here are some of the names we use:

  • navigation
  • related-news
  • time-navigation
  • push-custom-grid
  • double-broadcast-card

Creating the design variables

We created a file for the global CSS variables. It stores all our graphical charter elements, like:

  • Colors
  • Gradients
  • Shadows
  • Transition durations
  • Borders
  • Fonts
  • Media Queries

That file is our reference for all the UI and is edited only sparingly to keep the UX harmonized. All the CSS files must use those variables in priority to avoid new additions.

Here is an extract from our global variables file:

:root {
    --containerMaxWidth: 1000px;

    /* Main colors */
    --realBlack: #000;
    --black: #141414;
    --lightBlack: #212121;
    --darkBlue: #696f79;
    --blue: #d5d9e0;
    --grey: #e5e5e5;
    --lightGrey: #f9f9f9;
    --red: #ff004b;
    --white: #fff;
    --placeholderAds: #e3e9f2;

    /* Dark mode */
    --darkmodeRed: #ff236d;
    --darkmodeDarkBlue: #070720;
    --darkmodeBlue: #1c1d42;
    --darkmodeRedGradient: linear-gradient(135deg, #ff236d 0%, #ee5b35 100%);
    --darkmodePlaceholderAds: #151515;

    /* RGBa colors */
    --blackTransparent30: rgba(0, 0, 0, 0.3);
    --blackTransparent80: rgba(0, 0, 0, 0.8);

    /* Gradients */
    --redGradient: linear-gradient(135deg, #ff004b 0%, #ee5b35 100%);
    --purpleGradient: linear-gradient(135deg, #895de4 0%, #cb7995 100%);
    --blackGradient: linear-gradient(180deg, #44474d 0%, #161717 100%);

    /* Shadows */
    --purpleShadow: 0 10px 30px 0 rgba(167, 106, 191, 0.4);
    --greyShadow: 0 10px 30px 0 rgba(229, 229, 229, 0.4);

    /* Transitions */
    --transition300msEase: 0.3s ease;

    /* Border-radius */
    --mainBorderRadius: 15px;

    /* Fonts */
    --font-montserrat: "Montserrat", sans-serif;
}

/* Media queries */
@custom-media --media-mobile only screen and (max-width: 749px);
@custom-media --media-tablet only screen and (min-width: 750px);
@custom-media --media-desktop only screen and (min-width: 1024px);
Enter fullscreen mode Exit fullscreen mode

Static and dynamic component libraries

In collaboration with the design team, we created two component libraries, one static and one dynamic.

The static component library brings together all the components on Sketch, our design software. There, the components are static and not interactive. It allows us to easily create new pages based on the existing components. New components are automatically added to that library. It is mainly used by the design team and provides a good overview of all the currently available and designed components.

The dynamic component library is created from the static library and brings together all the components in their developed version. It's a tool for the UI development, inspired from StoryBook. In it, components are dynamic, interactive and use the same markup as the ones available on the website: nothing is duplicated. Static data is provided to every component so they can function independently. This library is use by all teams and provides an interactive overview of all the components available on the project.

Télé-Loisirs dynamic library


Optimizing Cumulative Layout Shift

"I was about to click on that! Why did it move? 😭"

Cumulative Layout Shifts can be disturbing for users. It happens when visible elements moves on the page because another element was added, removed or resized in that very same page. We identified the main causes of this behavior on our website:

  • Images without dimensions
  • Advertising spaces
  • Custom web fonts

Placeholders for images

During page load, images are often unavailable and the layout appears differently. Suddenly, elements jump from there to there because images are downloaded and displayed on the page. This is a normal behavior for a web page. In responsive web design, we can't fix the size of the images with the attributes width and height. The solution is to reserve the image area even if it is not yet loaded using the CSS ratio trick.

Imagine the following image inside a container:

<div class="picture">
    <img src="image.jpg" alt="I won't move the page, I promise" />
</div>
Enter fullscreen mode Exit fullscreen mode

Without dimensions, the content below the image will move during the page loads. Because we know the image ratio (calculated by: (height / width) * 100 = ratio), we figured we could prevent that behavior. For example, for a landscape image (16/9), the calculation is: (1080/1920) * 100 = 56.25. The container's height is calculated with the padding ratio, which is responsive (excellent to handle responsive videos by the way). The image is in absolute position, outside of the page flow and fills its container. Thus, having responsive images without layout shifts is possible with this simple technique.

.picture {
    position: relative;
    overflow: hidden;
    height: 0;
    padding-bottom: 56.25%;
}

.picture img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
Enter fullscreen mode Exit fullscreen mode

Placeholders for advertising

We use several formats for advertising which can often result in multiple layout changes on the site. In order to keep fixed advertising areas, we use the previously detailed solution which results in displaying placeholders to reserve areas on the page for the advertising elements. This result in an improved UX. Indeed, textual content and small iconographies can be added to help users easily identify advertising spaces.

Advertising placeholders on Télé-Loisirs

Custom web font loading

When a browser requests a font asset from a web server, any elements with styles invoking that font is hidden until the font asset has downloaded. This is known as the “Flash of Invisible Text,” or FOIT.

To avoid these flashes, we use the font-display CSS property. The swap value allows to use the next available system typeface in the font stack for text rendering until the custom font loads. Multiple values are available for different needs.

More information about font-display on CSS Tricks.


Improving web performances

Lighten dependencies

Like most websites, we had a lot of JavaScript dependencies, but now, with all the new ECMAScript features, the game has changed. Native JavaScript is powerful and can easily replace libraries like jQuery.

It's not about reinventing the wheel, but use Javascript native the most possible.

Javascript APIs are used in priority when they are natively supported by the browser. For older browsers, newer features are supported thanks to Polyfill.io.

Loading scripts

Scripts can have a negative impact on the loading time of a page. The location of script tags is important. We used native script tags in the <head> of the document with the defer attribute. That way, scripts will be fetched as soon as possible, but the browser will wait for the DOM tree to be complete before executing them.

More information about scripts loading in the article Efficiently load JavaScript with defer and async.

Content hash

To reduce the number of downloaded files by the browser on each page load, we use the webpack [contenthash] option. Webpack adds a unique hash based on the content of the asset. When the asset's content changes, the [contenthash] changes as well. Files compiled by webpack can remain cached until their content changes. We configured a cache of 1 year for images, JavaScript and CSS files. That way, no matter if another build is deployed to production, the hashes will remain the same for unchanged files and be updated only for edited files. In other words, the first time a user loads the page, all assets will be downloaded and cached by the browser. On the second page load, all unmodified assets will come from the browser's cache even if a new deployment occurred in between both page loads and all modified assets will be downloaded again.

Code-splitting and JavaScript modules

Code size is one of the biggest concerns for an application. Websites often combine all of their JavaScript into a single bundle. When JavaScript is served that way, the page load takes more time because it loads code that is not necessary for the current page. To serve the minimum JavaScript and CSS to the client, we split common JavaScript and CSS code.

In addition, to reduce code size for modern browsers, we introduced Javascript modules using the following approach:

  • Serve JavaScript modules with ES2015+ syntax for modern browsers (without Babel transpilation).
  • Serve JavaScript with ES5 syntax for older browsers (with Babel transpilation).

We've detailled a complete codelab on a dedicated article, Granular chunks and JavaScript modules for faster page loads.

Delivering only what the user needs, when he needs it

Today, websites have rich interfaces and display a lot of content. But users still don't see content outside the first screen. So, why would they need to load content they don't see yet? Lazy-loading scripts and contents on scroll can be highly beneficial for performance. We load Javascript bundles that contains only what the user needs and ideally, only what he can see on his screen.

We use the Web API IntersectionObserver to watch when a user is near a target element. For example, all the content below the first screen, which has Javascript dependencies, can be instanciated later. The rootMargin parameter allows us to specify when exactly to trigger elements, according to the user's need. We use a margin of 1 dynamic screen height to trigger the lazy-load like the following example:

const callback = (entries, observer) => {
    console.log('Function is triggered');
};
const options = {
    rootMargin: `0px 0px ${window.innerHeight}px 0px`
};
const observer = new IntersectionObserver(callback, options);

observer.observe(document.querySelector('#footer'));
Enter fullscreen mode Exit fullscreen mode

When the observer detects the target element, a dynamic import is triggered, all assets are loaded and the related JavaScript code is executed.

For browser support, we use the Polyfill.io IntersectionObserver

An open-source version of our observer module is available: lazy-observer on Github

SVG sprites

We use SVG files for icons to avoid HTTP requests and for their flexibility. Additionally, these icons are perfectly displayed no matter the pixel ratio; and animations can be done using CSS. To prevent icons from flickering during the page load, we use SVG sprites. Their content are inlined directly into the HTML. We are using the svg-chunk-webpack-plugin to automate the process of generating each sprites. Each page only imports its own svg sprite which has previously been optimized using svgo.

Sprite SVG preview

Responsive images

Users' screens are all different by sizes (watches, phones, tablets, laptops, desktops) and by pixel density (1x, 2x, 3x). Pictures made for a 1x pixel density can appear pixelated on devices with higher pixel density. Nomad devices generally have a slower connection. To provide the most suitable image format for any user, we need responsive images.

For images with the same ratio for all different breakpoints, the <img> tag, along with the srcset and size attributes are enough. For more complex use cases, the <picture> tag can be used, but it has the disadvantage of increasing the DOM size.

The following example displays an image that is compatible with different screen sizes, all formats (mobile, tablet and desktop), plus, the 1x and 2x pixel density.

<img src="image-64x90.jpg"
     sizes="(max-width: 750px) 64px, (max-width: 1023px) 64px, (min-width: 1024px) 64px"
     srcset="image-64x90.jpg 64w, image-128x180.jpg 128w"
     alt="My responsive image" />
Enter fullscreen mode Exit fullscreen mode

Better accessibility

Accessibility is essential on websites. It provides a better experience for all users. We use tools like Google Lighthouse to generate analysis reports, they contain useful information to improve it.

Some rules can greatly improve the score:

  • Use a minimum size for all links and buttons, along the padding property
  • Use <h1|2|3|4|5|6> for titles
  • Use <ul> or <ol> for lists
  • Use the <a> for link and the <button> for elements with Javascript action
  • Add the alt attribute on images
  • Add the title attribute on links
  • Add the aria-label attribute on <button|a> without text
  • Adjust the contrast for the color of the design
  • Respect HTML descendants (ul>li)

CSS tips for semantic

Monitoring

To monitor performance, we use two different tools:

SpeedCurve to analyze daily several of our main pages. This allows us to detect different types of issues:

  • Page load duration
  • Page size
  • Assets size
  • Number of requests
  • Lighthouse score

Google Page Speed Insights for occasional reports.

Google Page Speed Insights report


Rethinking CSS

CSS naming conventions

To improve maintainability and performance, we use a CSS naming convention: the flat hierarchy of selectors, inspired from BEM (Block Element Modifier) and FUN. CSS selectors are unique and shorter, which leads to smaller CSS files. To avoid class names that become too long quickly, we keep modifier classes independent from the block class name. In addition, the block class name and the element class name use the camelCase syntax with an hyphen as separator.

.blockName-elementName .modifier {}
Enter fullscreen mode Exit fullscreen mode

CSS variables for theming (dark mode)

The dark mode applies a custom theme to the website. It works by adding a class name to the <html> element. Our first approach was to override existing styles with rules that had higher specificities. We quickly noticed that this approach had issues. One of them being a considerable increase of the size of CSS files.

We've switched to native CSS variables for theming. That way the size of our CSS files remains lighter even if they contain both light and dark mode styles. Below, an example of the background color of an header element being overridden by another color when the dark mode is enabled.

.darkMode .header {
    --backgroundColor: #1c1d42;
}

.header {
    --backgroundColor: #212121;
    background-color: var(--backgroundColor);
}
Enter fullscreen mode Exit fullscreen mode

Support for IE11 had to be dropped to use native CSS variables and keep small CSS files. Otherwise the generated file would have duplicated all the CSS rules.

Télé-Loisirs dark mode


Code review

Team work are important part of our development processes. Code reviews helps improving the maintainability of a project, team spirit and allow everyone to increase theirs skills. To make the code review easier, merge requests needs to be small and their context needs to be respected. Merge requests are kept small which leads to a more efficient review.

Some of the rules we follow are listed in an article (in French) by @Julien Hatzig:

  • Promote asynchronous reviews.
  • Avoid making the review synchronous by asking for validation, but rather ask for a review of your work. This will put the reviewer in a better position which will leads to constructive feedback.
  • Add a context with a description in the header of the merge request. The reviewer doesn't know the topic you worked on.
  • Take the time to review other people's code.
  • Be benevolent in exchanges, favor the conditional in sentences, suggest solutions, describe problems.
  • Avoid sniper reviews.
  • Comments are not negative for the developer, they stimulate discussion and lead to improvements. Discuss more when necessary to find the most suitable solution.

Additional reading

💖 💪 🙅 🚩
yoriiis
Yoriiis

Posted on October 3, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related