CLS: Treading into the Unknown (Vol. 2)

Aseem Upadhyay
Engineering @ Housing/Proptiger/Makaan
7 min readFeb 11, 2022

Prologue

This is the second instalment in the series where we talk about what worked for us and what didn’t in our attempts to solve the problem of layout shifts in Housing.com.

Previous Chapter:

Analysing the report

Identifying problems in the first fold is easier as there are tools available to diagnose the issues with enough content on the internet to help solve them. However, the second fold is a unchartered territory and to start resolving the issues, all we have is a list of affected urls.

report presented by google

The URLs presented in the report is divided among three categories based on their respective CLS values:

  • Good — CLS score less than or equal to 0.10
  • Needs Improvement — CLS score is between 0.10 and 0.25
  • Bad — CLS score is greater than 0.25

Considering the report as our sample set, each URL being analysed brought forward the only question ::

Are client side components loaded appropriately?

Optimisation

Performance is an important aspect of any product, be it a website or some hardware. For us, one metric that we wanted to improve required reduction of our javascript payload by dividing the generated build into chunks, which brought us to the first problem statement

The number of chunks being generated by the bundler is directly proportional to the probability of a higher layout shift score

How?

For websites that serve their markup from a single file, there’s no hassle of handling the resources since they are all bound to be loaded together. This approach in principle shouldn’t cause any layout shifts because the elements/resources/components that are to be used at a later stage in time are already loaded and ready to be used.
However, this approach isn’t scalable and causes performance bottlenecks since a lot of bandwidth is being consumed for resolving functionalities that would be used in the future.

an example where various chunks are being called at runtime

To optimise this, chunking was brought into picture where the generated bundle is divided into chunks of different sizes and were loaded based on requirement. This solved the performance issues to some extent but introduced the problem of layout shifts.

Why?

Housing.com is a server side rendered application, which includes some part of its components/functionalities that are resolved on the client. Let’s take an example.

The component highlighted in the red box shows Recently Viewed Properties which tracks the properties that have been searched by the user and is visualised as a curated list. Since this component is dependent on a user action, it will be only resolved on the client and with chunking in picture, the module (javascript file) containing this functionality will only be requested once the section is in the viewport of the user.

Till the time the javascript file has not been downloaded, the container which needs to show the component stays empty because the code which will be used to render it is not present! Once the file is downloaded, the elements are attached to the DOM tree and voila! a section pops up in between causing a layout shift.

unhandled loading of module (left::component hasn’t loaded, right::component has loaded)

Remedy? For all the sections/components that are divided in chunks, we provided a placeholder space for the component which would be eventually replaced by the component itself. Since, components in the future have a pre-determined space (in this case height) allocated in the DOM tree, the elements that follow won’t undergo a layout shift because of its deterministic behaviour.

handled loading of a module (left::component hasn’t loaded, right::component has loaded)

That solves the issue, partially.

Client side APIs

The underlying principle behind solving a layout shift of any kind is to give space to the component that is bound to arrive. Now, this space can be specified either in terms of height or width. In the above use-case, the component contained in the file arrived later hence, the placeholder/loader given to the component saved it from causing a layout shift.

The same approach can be applied to components that render basis the data that is being provided via an API call. Till the time the API hasn’t resolved, we show a loader to the user, which has two benefits :

  • feedback to the user that some process is being executed in the background
  • a deterministic dimension to the component, so that when the component loads with the data, there is no change in the total height/width of the container component. To achieve this, the final state of the component is observed, let’s say the final dimension of the Recently Viewed Properties (mentioned above) is 350x200 (width x height) pixels, so, in order to avoid a layout shift we would require a loader of the same dimensions (350x200) to be shown till the time the API resolution is in progress.

Hence, for any component to be included in the website, the following decision tree was taken into consideration::

lifecycle of a client side rendered component

However, this approach only solves the case where data arrival is ensured .i.e. whenever an API is called, it will always provide us with some data. The case that fails is when there is no data available even after an API is called. For example, if there are no 5BHK flats available in a vicinity, all the components using this data will be rendered empty causing CLS as we had introduced loaders (explained in the tree above) which were matched to the final component dimensions.

By solving one issue, we introduced another..

Era of Fallbacks

How do we prevent the layout from shifting if there’s no data to show in the first place?

The meaning of a Placeholder varies depending on the context it is used for example, it can be used to give feedback to the user input, it can be used to fill up empty spaces (in our case, preventing CLS) etc.

Designing elements/cards to fill up spaces that are dynamic in nature requires consistent coordination between the design and the development team. Eventually, we were able to drill it down to 8 cards of various dimensions for both mobile and desktop. The idea behind fixed dimensions was, if these cards are to be placed dynamically there should be some commonality between them. Hence, any card that was to be put as a fallback for a component should adhere to any one of the given 8 templates.

placeholders for different sizes

This brings us to the next challenge:
If there are n number of available placeholders, how do we ensure that positions i and i+1 (consecutive indices in a list) are unique for content that is determined later in time (asynchronous)

This problem is fairly easy if the items in the list are deterministic .i.e. we know what type of an item would be present in the next index of our list (array) for example, in a list of numbers, we don’t need to check if any item in the list is a string.
The use case varies a bit wrt components. For instance, in a list of components there is no way of determining if a specific index of the list is empty if some/all of its components are asynchronous in nature.

Birth of the Placeholder Manager

The need of the hour was a generalised mechanism to determine that a placeholder if applied should have a design which is unique wrt its neighbouring elements and at the same time belong to any one of the aforementioned templates.
Considering our use case is asynchronous in nature, we wanted the placeholder manager to carry out atomic and synchronous operations.

How?
- Atomicity was achieved using Redux (a popular state management tool) where each dispatch made to its store is atomic in nature .i.e. if two synchronous dispatches x and y are made, the updates will be carried out in the same order.
- Synchronicity was achieved by pre-computing the placeholders wherever there was a need .i.e. post api resolution, the component already had the knowledge of which type of placeholder should be applied as the fallback.

Below is a representation of all the processes that were executed in the component

Once the system was in place, the fallbacks were applied and tested for layout shifts only to find out that quality of the website dipped as the sections that were earlier hidden due to unavailability of data were now highlighted with fallback banners.

An early representation of the idea:

LHS:: Fallback, RHS:: With Data

Hence, we were back to the drawing board, looking for ideas in a more balanced approach, taking care of product and layout shifts at the same time.

To be continued …

--

--