DEV Community

Cover image for How To Render Large Datasets In React without Killing Performance
Lucy Muturi for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

How To Render Large Datasets In React without Killing Performance

TL;DR: Rendering millions of data points in React challenges performance due to expensive DOM manipulation, often leading to crashes and low FPS. This can be overcome by implementing strategies like pagination, infinite scroll, or, for the most optimal React performance optimization, advanced React virtualization (windowing), which efficiently renders large datasets while maintaining a smooth 60FPS.

Rendering large datasets in React can be challenging due to performance issues. DOM manipulation is the most expensive operation on a web page, which drastically affects the performance, especially when the DOM tree is extremely large. A minor change requires reconstructing the whole DOM and then repainting it in the browser.

Maintaining the 60FPS with the large DOM tree also becomes challenging, especially on devices with limited memory, which often results in the page crashing.

React tries to solve this problem by using the Virtual DOM, which maintains an in-memory snapshot of the actual DOM. Any changes to the DOM are compared with this snapshot; only what has changed will be mutated. React also uses Batch updates, Memoization, and many other optimizations under the hood to optimize performance.

Even with all these optimizations, it is not feasible for React to efficiently render a large dataset or millions of data points.

In this article, we’ll explore effective techniques to handle this problem, ensuring your app remains fast and responsive. Whether dealing with thousands of rows or complex data structures, these methods will help you maintain optimal performance.

Pagination

The most basic and efficient way to deal with this problem is to avoid rendering the large dataset simultaneously by implementing pagination.

We render a limited set of records on the page at any given time, with the option to change the page and the limit of how many items to load.

Below is a simple example of pagination in React, showing 30 records at a time.

import "./styles.css";
import { faker } from "@faker-js/faker";
import { useState } from "react";

export default function Pagination() {
    const MAX_PER_PAGE = 30;
    const [currentPage, setCurrentPage] = useState(1);
    const data = new Array(100000).fill().map((_, index) => ({
        id: index,
        title: faker.lorem.words(5),
        body: faker.lorem.sentences(4),
    }));

    const onNext = () => {
        setCurrentPage((currentPage) => currentPage + 1);
    };

    const onPrev = () => {
        setCurrentPage((currentPage) => currentPage - 1);
    };

    const dataList = data.slice(
        (currentPage - 1) * MAX_PER_PAGE,
        currentPage * MAX_PER_PAGE
    );

    const showPrevPage = currentPage !== 1;
    const showNextPage = currentPage < data.length / MAX_PER_PAGE;
    return (
        <div className="App">
            <h1>Page: {currentPage}</h1>
            <div className="products-container">
                {dataList.map((item) => (
                    <div key={item.id} className="product">
                        <h3>
                            {item.title} - {item.id}
                        </h3>
                        <p>{item.body}</p>
                    </div>
                ))}
            </div>
            <div>
                {showPrevPage && <button onClick={onPrev}>Prev</button>}
                {showNextPage && <button onClick={onNext}>Next</button>}
            </div>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Advantages

  • Render a small set of records with flexibility to extend the limit.
  • Performance efficient and maintains the 60fps.
  • Works well even on resource-constrained devices.

Disadvantages

  • It is not the best user experience as the user has to navigate each page manually.

Infinite-scroll

To improve the user experience, the user has to manually change the page to fetch the new set of records. We can implement the infinite-scroll where the new set of records is fetched automatically when the user has finished scrolling through the current set of records, or the scroll is about to end.

This improves the user experience as the user can see the new record as we scroll.

import "./styles.css";
import { faker } from "@faker-js/faker";
import { useState, useEffect, useRef, useMemo } from "react";

export default function InfiniteScroll() {
    const MAX_PER_PAGE = 30;
    const [currentPage, setCurrentPage] = useState(1);
    const listRef = useRef();

    const data = useMemo(() => {
        return new Array(100).fill().map((_, index) => ({
            id: index,
            title: faker.lorem.words(5),
            body: faker.lorem.sentences(4),
        }));
    }, []);

    useEffect(() => {
        const onScroll = () => {
            if (
                listRef.current &&
                window.innerHeight + window.scrollY >=
                    listRef.current.offsetTop + listRef.current.offsetHeight
            ) {
                setCurrentPage((currentPage) =>
                    currentPage < Math.ceil(data.length / MAX_PER_PAGE)
                        ? currentPage + 1
                        : currentPage
                );
            } };

        window.addEventListener("scroll", onScroll);
        return () => window.removeEventListener("scroll", onScroll);
    }, [data.length]);

    const dataList = data.slice(
        (currentPage - 1) * MAX_PER_PAGE,
        currentPage * MAX_PER_PAGE
    );

    return (
        <div className="App">
            <h1>Page: {currentPage}</h1>
            <div className="products-container" ref={listRef}>
                {dataList.map((item) => (
                    <div key={item.id} className="product">
                        <h3>
                            {item.title} - {item.id}
                        </h3>
                        <p>{item.body}</p>
                    </div>
                ))}
            </div>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Advantages

  • Works well for a small set of records
  • Great user experience as the user can browse through all the records without taking action.

Disadvantages

  • Can lead to performance issues if a large set of records is loaded through infinite scrolling.
  • Maintaining the FPS also becomes challenging, especially when we have media.
  • Cannot directly jump to the given page or record, and needs to use the search to filter out records.

Virtualization or Windowing: The most optimal way.

We can create the best solution by taking the best of the above two approaches, where we render a small set of data (Pagination) and still allow the user to scroll infinitely to load new records (Infinite-scroll).

This can be done by using list virtualization or windowing, where we render a small set of records (that fit the screen) and maintain a buffer. We lazy-load the remaining ones as we scroll through the current record, while removing the hidden ones.

This is like windowing, where we shift through a large set of records in either direction with a small subset.

In theory, this is how virtualization works:

  • Virtual window: Maintain a small set of records and a buffer at both ends, before and after the current subset.
  • Scroll simulation: Use the placeholder elements to maintain the full scroll height. Adjust the heights as the actual element renders.
  • Dynamic positioning: Each element is positioned through CSS transform.
  • Garbage collection: Recycle the elements that are no longer visible.

I have used the react-virtualized package in the example below to implement the virtualization.

import React, { useMemo } from "react";
import { Grid, AutoSizer, ColumnSizer } from "react-virtualized";
import { faker } from "@faker-js/faker";

import "./virtualized.css";

const VirtualizedProductList = ({
      height = 600,
      width = 1200,
      columnWidth = 300,
      rowHeight = 300,
      overscanRowCount = 2,
}) => {
    const sampleProducts = useMemo(
        () =>
            new Array(10000).fill().map((_, index) => ({
                id: index,
                title: faker.lorem.words(5),
                body: faker.lorem.sentences(4),
            })),
        []
    );

    const COLUMNS_PER_ROW = 3;
    const totalRows = Math.ceil(sampleProducts.length / COLUMNS_PER_ROW);

    const cellRenderer = ({ columnIndex, key, rowIndex, style }) => {
        const productIndex = rowIndex * COLUMNS_PER_ROW + columnIndex;
        const product = sampleProducts[productIndex];

        // Return empty cell if no product for this position
        if (!product) {
            return <div key={key} style={style} />;
        }

        return (
            <div
                key={key}
                style={{ ...style, height: style.height - 20 }}
                className={"cellWrapper"}
            >
                <div className={"productCard"}>
                    <h3>
                        {product?.title} - {product?.id}
                    </h3>
                    <p>{product?.body}</p>
                </div>
            </div>
        );
    };

    return (
        <div className={"virtualizedProductList"}>
            <div className={"header"}>
                <h2>Product Catalog</h2>
                <div className={"footer"}>
                    <div className={"stats"}>
                        Total Products: {sampleProducts.length} | Total Rows: {totalRows} |
                        Rows visible: ~10
                    </div>
                </div>
            </div>

            <div className={"gridContainer"}>
                <AutoSizer>
                    {({ height: autoHeight }) => (
                        <ColumnSizer
                            columnMaxWidth={columnWidth}
                            columnMinWidth={50}
                            columnCount={COLUMNS_PER_ROW}
                            width={width}
                        >
                            {({ adjustedWidth, getColumnWidth, registerChild }) => (
                                <Grid
                                    ref={registerChild}
                                    cellRenderer={cellRenderer}
                                    columnCount={COLUMNS_PER_ROW}
                                    columnWidth={getColumnWidth}
                                    height={Math.min(height, autoHeight)}
                                    overscanRowCount={overscanRowCount}
                                    rowCount={totalRows}
                                    rowHeight={rowHeight}
                                    width={adjustedWidth}
                                    className={"grid"}
                                    style={{
                                        outline: "none",
                                    }}
                                />
                            )}
                        </ColumnSizer>
                    )}
                </AutoSizer>
            </div>
        </div>
    );
};

export default VirtualizedProductList;
Enter fullscreen mode Exit fullscreen mode

Advantages

  • Maintains a small dataset while providing infinite-scrolling
  • As the window size is small, we can instantly render the elements regardless of the size of the dataset.
  • Maintains the 60fps even with the complex items

Disadvantages

  • Sometimes it becomes challenging to maintain the dynamic height of the elements. It works best when the fixed height is defined upfront, which keeps all the calculations like scroll height constant.
  • Cannot jump to a given item directly, need to scroll to that element.
  • Using virtualization in lists or dropdowns becomes challenging because the selected option has to be present/rendered in the DOM.

FAQs

Q1: What is the primary challenge when rendering a large dataset in React?

The main challenge stems from the expense of DOM manipulation. A large DOM tree, especially with millions of data points, can significantly affect performance, making it difficult to maintain 60 frames per second (FPS) and potentially leading to page crashes, particularly on devices with limited memory.

Q2: What is the best way to render large datasets in React?

The best way to render large datasets in React is to use techniques like pagination, infinite scroll, and virtualization to ensure optimal performance.

Q3: How does pagination help in rendering large datasets in React?

Pagination helps by breaking down the dataset into smaller, manageable chunks, reducing the amount of data rendered at once and improving performance.

Q4: What is virtualization in React, and how does it work?

Virtualization in React involves rendering only the visible portion of the dataset, dynamically loading and unloading data as the user scrolls, significantly enhancing performance.

Q5: Can infinite scroll be used for large datasets in React?

Yes, infinite scroll can be used for large datasets in React. It loads data incrementally as the user scrolls, providing a seamless user experience without overwhelming the browser.

Conclusion

Thank you for reading! Rendering large datasets in React can be efficiently managed using techniques like pagination, infinite scroll, and virtualization. By implementing these strategies, you can ensure your application remains performant and user-friendly.

Each has its advantages and disadvantages, and neither is the best solution. Each one serves a purpose; use it according to the challenge you are trying to tackle.

Explore Syncfusion’s React components for optimized performance and take your app to the next level.

Related Blog

This article was originally published at Syncfusion.com.

Top comments (0)