article cover

Performance

Optimizing large pages

~ 9 minute read

Adrian Apostol
Senior Software Engineer

15 Aug 2022

The latest developments of the hardware and UI libraries allow any frontend developer to create very dynamic and cpu intensive pages. Even when using smartphones or tablets there's still plenty of power to have 60fps on websites that are heavy on the UI side.

There are still some situations where the page may become unresponsive or sluggish. One of these situations is when we have a page that has "infinite" scroll and contains a lot of interactive elements, or elements that refresh their content every X seconds.

Let's consider this example: we have a lot of interactive tables (mouseover effects, regular refresh of data, inline editing of data etc.) on a single page that has vertical scroll. If these tables refresh their content every 5 seconds and do some basic calculations with that data in order to redraw the entire content of the table, that would require quite some CPU time. The page may still be responsive for several tables, but once we go to tens or hundreds of tables, we need a way to manage the changes that we do on the page.

One way to handle this is to only update & display the elements that are visible. Basically we will create a way of checking the position of each element we're interested in (in our case dynamic tables) compared to the viewport. We're going to check this every time we need. It will usually be whenever the page is scrolled. We may also want to check this when the DOM changes just in case one of the tables changes its height and forces other tables to get out or in the viewport.

As always, there's actually multiple ways to do this, so we're going to present two of them.

The classic solution: create a scrollHub that allows us to register components to be verified whenever the user scrolls

This is fairly simple to actually implement. The main functionality we need to cover is:

  • keep a list of components we need to check against the viewport along with a callback
  • allow components to be added / removed
  • listen on scroll event
  • when scrolled, check each components position compared to viewport

Keeping a list of components along with its callback is easy, so we won't cover that in detail. This can be done easily with a simple array, along with some operations for adding / removing elements. The most interesting things are actually calculating the position for each element and checking if it's in the viewport.

In order to get a HTMLElement position we can actually use the getBoundingClientRect() function. This will return a top and bottom property relative to the viewport. So getting all our elements positions is as simple as:

1const allPositions = components.map(component => {
2    const { top, bottom } = component.element.getBoundingClientRect();
3    return { component, top, bottom };
4});
5    

Once we have this, we can go through the entire list and check if it's inside the viewport. In order to check if the element is intersecting the viewport we need to check 3 conditions:

  1. if the TOP margin is inside the viewport - at least the top part of the element is inside the viewport
  2. if the BOTTOM margin is inside the viewport - at least the bottom part of the element is inside the viewport
  3. if the TOP is above the viewport and the BOTTOM is below the viewport - element is actually larger than the viewport so the element "contains" the viewport

This can be done like this:

1function checkIfInViewport(top: number, bottom: number, offset: number = window.innerHeight): boolean {
2    return (
3        (top >= 0 - offset && top <= window.innerHeight + offset)
4        || (bottom >= 0 - offset && bottom <= window.innerHeight + offset)
5        || (top <= 0 - offset && bottom >= window.innerHeight + offset)
6    );
7}
8    

You can notice here that we're actually using an offset as well, so we consider a component to be in the viewport before it actually reaches the top and bottom so the component is already visible shortly before we scroll that component into the viewport.

Then we just need to go over all of the positions and check each component:

1allPositions.forEach(pos => {
2    const isInViewport = checkIfInViewport(pos.top, pos.bottom);
3    pos.component.callback(isInViewport);
4});
5    

The modern solution: create a hub that uses the intersection observer

A more modern solution uses the IntersectionObserver. The benefit of this method is that the position and intersecting calculations are actually done for us, and not on our main JS thread. This helps a lot with the performance of the solution and it allows a cleaner code.

Regarding the overall solution it will be similar to the classic one, but we'll only need to manage our list of components and setup the observer itself. The scrolling and calculations will be done by the IntersectionObserver.

In order to setup the observer you need to run its constructor:

1const options = {
2    root: element,
3    rootMargin: '1000px 0px 1000px 0px',
4    threshold: [0, 1.0],
5};
6
7const observer = new IntersectionObserver(observerCallback, options);
8    

There are several options you can send to the constructor, the most notable ones include the "root" element to use (by default it will be the viewport), a "rootMargin" (we want to keep our components visible right before they actually get inside the viewport) and the "threshold" (what limit needs to be crossed so the callback is called).

The observerCallback will receive a list of all the entries the observer is watching and will let us know if it's intersecting or not. Something like this:

1function observerCallback(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
2    entries.forEach((entry: IntersectionObserverEntry) => {
3            entry.target => the element
4            entry.isIntersecting => if it's intersecting or not
5    });
6}
7    

More details on the IntersectionObserver can be found on the MDN page (Intersection Observer API).

An important note on this is that the intersection observer is not supported at all in IE - hopefully this will become a less important note in time.

By using a scrollHub or the IntersectionObserver you can keep your app usable and responsive when there's a lot of interactive and dynamic elements on a long page. By somehow disabling the elements that are not visible, you're not using CPU and memory on the main thread for updating those unnecessarily, allowing a smooth experience for your users.

Written by

Adrian Apostol

Senior Software Engineer

Share this article on

We create value through AI and digital software engineering in a culture of empowerment that enables new perspective and innovation.

OFFICE

24 George Barițiu street, 400027, Cluj-Napoca, Cluj, Romania

39 Sfântul Andrei street, Palas Campus, B1 block, 5th floor, 700032, Iasi, Iasi, Romania