DEV Community

Cover image for The Simple Trick That Makes a List of 50,000 Render Smoothly
Murad Haneya
Murad Haneya

Posted on

The Simple Trick That Makes a List of 50,000 Render Smoothly

Big lists don’t have to feel big. Some apps make 50,000 items feel like six.

Why is rendering 50,000 items bad?

This might seem obvious with such an exaggerated number, but what about a much smaller number like 1000? The number is arbitrary and doesn't mean anything because it carries a different weight depending on the device and its resources.

The DOM (Document Object Model) represents the HTML structure of a web page as a tree of nodes and objects. This is created by the browser's rendering engine - we don't have to worry about it. When DOM nodes change, the browser may need to recalculate styles, layout, and repaint parts of the page. The more nodes involved, the more expensive this process becomes.

This is where the Virtual DOM comes in. It's basically an in-memory representation of the real DOM and does not interact with the browser's rendering engine. When a state change occurs, React first updates the Virtual DOM instead of the real DOM. It will then use a "diffing" algorithm to identify the necessary changes. This selective mechanism significantly improves performance and predictability by reducing re-renders and minimizing direct manipulation of the real DOM.

Let's look the following example where we have a Select of shipping ports as options:

<select name="ports">
  <option value="NLRTM">Rotterdam</option>
  <option value="DEHAM">Hamburg</option>
  <option value="FRLHV">Le Havre</option>
</select>

Enter fullscreen mode Exit fullscreen mode

This is how the "Rotterdam" option is represented in the virtual DOM (simplified for clarity):

{
  type: 'option',
  props: {
    value: 'NLRTM',
    children: 'Rotterdam'
  },
  key: null,
  stateNode: HTMLOptionElement, // Reference to the real DOM node
  return: FiberNode,            // Pointer to the parent <select>
  sibling: FiberNode,           // Pointer to "Hamburg"
  // ... dozens of other internal reconciliation properties
}

Enter fullscreen mode Exit fullscreen mode

As you can see, the metadata React needs to manage for each node is significant. If we have 1000 ports in our Select, then React's memory fills up with 1000 of these objects. Every time this component is rendered, React may have to traverse this large linked list of objects. Not to mention the JavaScript and C++ objects created by the browser engine.

While 1,000 items won’t crash a modern machine, it's enough to exceed a 16ms frame budget. That’s enough to cause dropped frames, especially when combined with user interactions like scrolling or typing. This is even worse on mobile phones since they're more limited on resources. Scrolling a long, non-virtualized list on mobile often feels heavy and results in frame drops.

One important thing to remember is that a complex web page consists of many components that have their own overhead to consider. I say this because we often design, develop, and test new components in isolation, which hides cumulative cost.

Why memoization isn't enough

Most React developers would reach for memoization here, and I understand why. It feels right because fewer re-renders sounds like less work. But the key thing is that memoization stops React from re-running code and logic, it does not reduce how much the browser has to draw. If 1,000 elements exist, the browser still deals with 1,000 elements.

This becomes obvious during interactions like scrolling. Even if React doesn’t re-render, the browser still has to calculate layout and paint every visible element on each frame. With enough elements, that alone is enough to cause jank.

Render less, not faster

In an ideal world, the API you're consuming would give you the ability to paginate, filter, or search to reduce the number of results returned. If we take our port selector as an example, instead of returning every port in the world, or every port in Europe, it would return the ports in a selected country. This would result in, say, 50 options.

But you don’t always have control over the API you’re consuming. If it’s an internal API, it may be maintained by a different team, and if it’s a third-party API, you’re at the mercy of someone else entirely.

In those cases, the responsibility shifts to the UI. Even if you receive a large dataset, you don’t have to render all of it at once. Rendering less might mean only showing what’s visible on screen, delaying work until the user interacts, or replacing large dropdowns with search-driven interfaces.

The goal isn’t to make rendering faster, but to avoid doing unnecessary work in the first place. When you reduce the number of elements the browser has to deal with, everything else gets cheaper by default.

We do this with a technique called "windowing", also known as virtualization.

What is Virtualization?

This term sounds more intimidating than it actually is. Virtualization is essentially rendering only what's visible on the viewport. The viewport can be any element that displays a list, so in the case of a Select component, it's the scrollable container that displays the options. This means that if the viewport is displaying six elements then that's exactly what browser renders, no matter how large your dataset is. We're not talking about hiding elements but actually removing them from the DOM when they're not on screen.

While virtualization is powerful, it may not be suitable for your use case. For example, because items not on screen are physically removed from the DOM, the browser’s native "Find on Page" won't be able to see them. Also, if your list items have unpredictable heights that change after they render (e.g. an image loading in), virtualization becomes much more complex to implement. If your list is only a few hundred items, the complexity of a virtualization library might be overkill.

Summary

Whether you’re building a chat feature, a social feed, or even just a Select component, virtualization can make a huge difference in how smooth your app feels. If you want to try it out, I recommend using a well-established library. For React, react-window and Virtuoso are both solid choices.

If you found this post useful and want to support more content like this, you can buy me a coffee ☕ or share it with a friend or colleague.


Cover image by Edson Junior on Unsplash

Top comments (0)