Virtualization with fixed size items (part 1)
Tags: javascript, tutorial, performance, react
Published: 2022-06-01 19:13:26

Have you ever heard about **virtualization**? Do you know how it works under the hood?

If it's not the case or if you want to refresh your knowledge, let's do it!

> **Note:** In this article, I only do the implementation when dealing with fixed item size. Vertical and horizontal layout will be handled.
> In a second article, we will see how to manage when we have variable item height.
> 
> All the logic will be extracted in a JS class, so that you can use it in your favorite library. At the end, I do it with **React**.

----

## What is virtualization?

Virtualization is the fact to put in the DOM only elements that are displayed on the user screen. In reality, there are some elements before and after to have a smooth scroll.

_Why do we do that?_
If you put to much elements in the DOM you can have some performance issues and a bad user experience due to:
- a lot of DOM nodes in memory
- style calculation and paint cost


_Who uses that?_
This in a technic that is really used in the industry, often in combination with infinite scroll. For example sites like **Twitter**, **Instagram** or **Reddit** uses it.

> **Note:** `dev.to` seems not use it, which causes some trouble when we scroll a lot in the page. For example, when we have 650 articles we can see some issues:

![Dev.to performance issue with 650 articles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1yf2hbdup9tgwku7syze.gif)

## Items with fixed height

For the first article we are going to do virtualization with items having the **same height**.

Before, going "deep" in the implementation, it's important to understand the strategy we gonna have.

![Virtualization explanation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ychxadnzeeo56uhy6exc.png)

As you can see in the image, there are multiple things to understand:
- **container**: it's the element that will contain the list.
- **visible items**: they are the items that are currently visible to the user. They are in blue in the image.
- **invisible items present in the DOM**: they are extra items that are currently not visible to the user, but are present on the DOM to have a smooth  scroll.
- **invisible items**: they are items that are in the list but not present in the DOM. They will be in the DOM if they are in the range of items to put in the DOM, because are in the two previous category, when scrolling. 

----

### Get first and last index

Let's do some Maths, simple one don't be afraid, to calculate the first visible item index:

```javascript
// Rounding down if the first item is half displayed
// for example
const firstVisibleIndex = Math.floor(scrollOffset / itemSize);
```

You see, nothing complicated. Let's do the same thing to get the last index:

```javascript
// Rounding down if the last item is half displayed
// for example
const lastVisibleIndex = Math.floor(
  (scrollOffset + window.height) / itemSize
);
```

----

### Extra items

Now let's talk about extra items. As seen previously, most of the time we will add extra item before and after the display ones. It will improve the smoothness of the scroll and not display big white screen when scrolling fast.

So the first index of present element is:

```js
// We do not want to have negative index
// So let's take the max of the calculation and 0
const firstPresentIndex = Math.max(
  firstVisibleIndex - extraItems,
  0
);
```

And the last index of present element is:

```js
// We do not want to have an index superior to 
// the maximum item number
// So let's take the min of the calculation and `itemNumber`
const lastPresentIndex = Math.min(
  lastVisibleIndex + extraItems,
  itemNumber
);
```

----

### Positioning of items

We will need to place the items that are presents manually in the list element. The solution that I chose is to set the list element with `position: relative` and the items with `position: absolute`.

I you are not used to `relative / absolute` positioning, here is a little image to explain it:

![Relative/absolute positioning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enxpinjy6xwy9u4qd0r9.png)
 

For our virtualization, the items which are in `absolute` position, are positioned relatively to the list element (which have `relative` position) thanks to `top` or `left` css properties in function of the list layout.

Then the list will scroll inside the container thanks to `overflow: auto`.

The first thing to do is to set the list style:

```js
const getListStyle = () => {
  const listSize = this.itemNumber * this.itemSize;

  if (this.isVerticalLayout()) {
    // When dealing with vertical layout
    // it's the height that we configure
    return {
      height: listSize,
      position: "relative",
    };
  }

  // Otherwise it's the width
  return {
    width: listSize,
    position: "relative",
  };
};
```

And now let's do a method to get an item style by its index:

```js
const getItemStyle = (itemIndex) => {
  const itemPosition = itemIndex * this.itemSize;

  if (this.isVerticalLayout()) {
    // When dealing with vertical layout
    // the item is positioned with the
    // `top` property
    return {
      height: this.itemSize,
      width: "100%",
      position: "absolute",
      top: itemPosition,
    };
  }

  // Otherwise with the `left` property
  return {
    height: "100%",
    width: this.itemSize,
    position: "absolute",
    left: itemPosition,
  };
};

```

----

### Full implementation in a class

Let's implement all that we have seen previously in an `FixedVirtualization` class:

```js
class FixedVirtualization {
  constructor({
    containerHeight,
    containerWidth,
    itemNumber,
    itemSize,
    extraItems,
    layout,
  }) {
    this.containerHeight = containerHeight;
    this.containerWidth = containerWidth;
    this.itemNumber = itemNumber;
    this.itemSize = itemSize;
    this.extraItems = extraItems;
    this.layout = layout;
  }

  isVerticalLayout = () => {
    return this.layout === "vertical";
  };

  getListStyle = () => {
    const listSize = this.itemNumber * this.itemSize;

    if (this.isVerticalLayout()) {
      // When dealing with vertical layout
      // it's the height that we configure
      return {
        height: listSize,
        position: "relative",
      };
    }

    // Otherwise it's the width
    return {
      width: listSize,
      position: "relative",
    };
  };

  getItemStyle = (itemIndex) => {
    const itemPosition = itemIndex * this.itemSize;

    if (this.isVerticalLayout()) {
      // When dealing with vertical layout
      // the item is positioned with the
      // `top` property
      return {
        height: this.itemSize,
        width: "100%",
        position: "absolute",
        top: itemPosition,
      };
    }

    // Otherwise with the `left` property
    return {
      height: "100%",
      width: this.itemSize,
      position: "absolute",
      left: itemPosition,
    };
  };

  getFirstItemIndex = (scrollOffset) => {
    return Math.max(
      Math.floor(scrollOffset / this.itemSize) -
        this.extraItems,
      0
    );
  };

  getLastItemIndex = (scrollOffset) => {
    return Math.min(
      Math.floor(
        (scrollOffset + this.containerHeight) /
          this.itemSize
      ) + this.extraItems,
      this.itemNumber
    );
  };
}
```

And here we go! Only one more step before we have something fully functional.

----

### Detects scroll

Now, we need to watch when the user is scrolling inside the container. 

Let's simply add a listener on the `scroll` event of our container element:

```js
// Just register an event listener on `scroll` event
// In React will be done inside a `useEffect` or 
// directly with an `onScroll` prop
const onScroll = () => {
  // Update a state or retrigger rendering of items
  // In React will be done with a `useState` to set the offset
};
container.addEventListener("scroll", onScroll);

// You will probably have to add a `removeEventListener`
```

----

### Let's play

Now that we have the logic of virtualization extracted in `FixedVirtualization` and know that we need to re-render our items when scrolling in the container element, let's do it in **React**.

The API that I decided to do, is to expose a `List` component with the following props:
- `layout`: the layout of our list, `vertical` or `horizontal`. By default `vertical`
- `containerHeight`: the height of the container
- `containerWidth`: the width of the container
- `itemNumber`: the number of items that is in the list
- `itemSize`: the size of an item. The height for vertical layout, otherwise the item width.
- `renderItem`: a callback to render an item. The index of the item and the style to spread on the item will be passed as parameters.

This is how you will use it:

```js
function App() {
  return (
    <List
      containerHeight={400}
      containerWidth={600}
      itemNumber={1000}
      itemHeight={50}
      renderItem={({ index, style }) => (
        <div
          key={index}
          style={{
            ...style,
            // Just put a border to see each item
            border: "1px solid black",
          }}
        >
          {index}
        </div>
      )}
    />
  );
}
```

And here is the implementation of the `List` component:


```js
function List({
  renderItem,
  containerHeight,
  containerWidth,
  itemNumber,
  itemSize,
  layout = "vertical",
}) {
  const [fixedVirtualization] = useState(
    () =>
      new FixedVirtualization({
        containerHeight,
        itemNumber,
        itemSize,
        extraItems: 10,
        layout,
      })
  );
  // We put the offset in a state
  // And get the right items to display at each render
  // and their styles
  const [scrollOffset, setScrollOffset] = useState(0);

  const firstIndex =
    fixedVirtualization.getFirstItemIndex(scrollOffset);
  const lastIndex =
    fixedVirtualization.getLastItemIndex(scrollOffset);

  // Let's create an array of the items
  // which are present in the DOM
  const items = [];
  for (
    let index = firstIndex;
    index <= lastIndex;
    index++
  ) {
    items.push(
      renderItem({
        index,
        style: fixedVirtualization.getItemStyle(index),
      })
    );
  }

  // Let's create an `onScroll` callback
  // We `useCallback` it only to have a stable ref for
  // the throttling which is for performance reasons
  const onScroll = useCallback(
    throttle(250, (e) => {
      const { scrollTop, scrollLeft } = e.target;
      setScrollOffset(
        layout === "vertical" ? scrollTop : scrollLeft
      );
    }),
    []
  );

  return (
    <div
      style={{
        height: containerHeight,
        width: containerWidth,
        overflow: "auto",
      }}
      onScroll={onScroll}
    >
      <div style={fixedVirtualization.getListStyle()}>
        {items}
      </div>
    </div>
  );
}
```

> **Note:** Here I use the `throttle-debounce` library.

----

## Conclusion

You can play with the complete code with this sandbox:
{% codesandbox virtualization-zud5wy %}

In a following article, you will see how to manage when we have items with different height.




----

Do not hesitate to comment and if you want to see more, you can follow me on [Twitch](https://twitch.tv/rainbowapps ) or go to my [Website](https://www.romaintrotard.com).