Aug 15, 2025·8 min read

React virtualization libraries for fast long lists

React virtualization libraries keep long lists smooth on weak laptops. Learn where windowing pays off most in tables, trees, and search results.

React virtualization libraries for fast long lists

Why long lists feel slow

A long list feels slow long before you hit ten thousand items. The browser has to create a DOM node for every row, measure it, place it on the page, and paint it. If each row has text, buttons, icons, badges, and thumbnails, that work multiplies fast.

People often blame React first, but the browser usually takes a big share of the hit. A page with 5,000 rows might look simple, yet each row can include several elements. That turns one list into tens of thousands of things the browser must track while you scroll.

Scroll adds another cost. Many apps update hover states, lazy images, sticky headers, selection boxes, or row details while the user moves the wheel. If every scroll step triggers React updates across a large component tree, frames start to drop. You feel it as stutter, delayed clicks, or a list that sticks for a split second.

Weak laptops show this sooner because they have less CPU headroom, slower memory, and often integrated graphics. Open a few tabs, a chat app, and a video call, and the margin disappears. A list that feels fine on a newer machine can feel rough on a five-year-old office laptop.

A simple example makes the problem obvious. Imagine a search page that renders 2,000 results at once, and each result shows an avatar, two badges, a status dot, and an action menu. Even before the user clicks anything, the page spends time drawing far more than the screen can show.

That is why React virtualization libraries matter. They cut the work by keeping only the visible rows, plus a small buffer, in the page. If a person can see 20 items, rendering 2,000 is usually wasted effort.

Where windowing helps most

Windowing pays off when a page holds far more items than the screen can show. If someone scrolls through 5,000 rows, the browser still spends time on hidden rows unless you stop it. On a weak laptop, that turns into delayed input, choppy scrolling, and a tab that feels heavier than it should.

This is where React virtualization libraries make sense. They keep only a small slice of items in the DOM and swap them as the user moves. If a screen shows 25 rows, the app may render 30 or 40 instead of all 5,000.

The biggest wins usually show up in places like these:

  • data tables with many similar rows
  • search results that users scan quickly
  • event logs or activity feeds that keep growing
  • trees with lots of expanded nodes

Repeated rows benefit first because the layout is predictable. A table with simple cells is a good example. Most users see only a few dozen rows at once, so rendering thousands of hidden ones gives them nothing and costs you smooth scrolling.

Tables often feel faster right away because row hover states, selection, and sticky UI have less work to do. Search results also improve fast. People type, filter, and scroll in short bursts, so keeping the DOM small helps the page react without that slight pause after every update.

Trees still benefit, but the gain is less tidy. Open and closed branches change row positions, and some nodes may have different heights. Windowing still helps when people browse deep file structures or long menus, but it needs more care than a flat table.

Short lists are a different story. If a page shows 20 or 40 items, plain rendering is usually simpler and already fast enough. Windowing adds setup, measurement issues, and focus quirks, so it is hard to justify unless users really scroll far.

Pick a library that fits the job

A bad fit shows up fast when the list gets big. You can make almost any tool work, but weak laptops punish the wrong choice with stutter, jumpy scroll, or lost focus.

Among React virtualization libraries, the simplest option is often the best one. If every row has the same height, pick react-window. It stays small, does one job well, and keeps the mental load low.

A product list is a good example. If each item row is 48px tall and the layout does not change, react-window is usually enough. You get smooth scrolling without carrying extra features you may never use.

If row height changes a lot, React Virtuoso is easier to live with. It handles messages, comments, search cards, and other mixed content better because it deals with size changes for you. That saves time, and it avoids the awkward gaps and jumpy repositioning you often see when heights are only guessed.

TanStack Virtual fits teams that want more control. It works well when you build a table, grid, or tree with your own structure and need to tune how items render and measure. It asks for more setup, but it gives you room to shape the behavior instead of fighting a fixed pattern.

A simple way to choose:

  • Use react-window for plain lists with fixed-height rows.
  • Use React Virtuoso when row height changes often.
  • Use TanStack Virtual when you need a table, grid, or tree with tighter control.

Do not stop at scrolling speed. Check sticky headers, keyboard focus, and overscan before you commit.

Sticky headers matter in tables because users lose context fast when column names disappear. Keyboard focus matters in search results and trees, where people move with arrow keys, tab, or enter. Overscan matters because rendering a few extra rows can make fast scrolling feel smoother, but too much overscan brings back the heavy DOM you were trying to avoid.

If you are unsure, build the smallest real screen first. A fixed-height list, a table with a sticky header, or a tree with expand and collapse will tell you more in 20 minutes than a long feature chart.

How to add virtualization step by step

Start with the viewport, not the data. Measure how tall the list area is, then estimate how many rows fit on screen. If the panel shows about 12 items, you do not need to render 2,000. You need 12, plus a small safety buffer so scrolling does not flash empty space.

A simple setup usually works like this:

  1. Measure the list container height.
  2. Set a row height, or a close estimate.
  3. Render only the visible rows plus a few above and below.
  4. Keep row components small, with stable props.
  5. Fetch the next chunk before the user hits the end.

That buffer matters more than people expect. Too small, and the list feels jumpy when someone scrolls fast. Too large, and you lose the benefit of windowing in React because the browser still has to paint too many nodes. On an older laptop, even a difference of 20 or 30 extra rows can change how smooth the page feels.

Keep each row boring. A row should mostly display data, handle a click, and stop there. Heavy formatting, deep nested components, and inline functions in every item add cost on every scroll. If a row shows the same avatar, label, or status for long stretches, memoize that content so React does not keep rebuilding it.

Stable identity also helps. Use a real item id, not the array index, when the list can reorder or load more results. That keeps selection, hover state, and expanded details from jumping to the wrong row.

For loading, do not wait until the last item appears. Trigger the next fetch a bit earlier, such as when the user gets within one or two screens of the end. A search screen with 5,000 results feels much faster when new rows are ready before the user notices the boundary.

Most React virtualization libraries handle the math for you. Your part is simpler: measure the visible area, keep rows cheap to render, and make sure new data arrives before the list feels empty.

What changes in tables

Pick The Right Virtualizer
Talk through react-window, React Virtuoso, or TanStack Virtual before your team builds the wrong thing.

A table breaks more easily than a simple list. Rows still matter, but column alignment, sticky parts, and cell behavior matter just as much.

Windowing usually helps a table when you show many rows with the same structure. It cuts the number of mounted rows, which lowers paint and layout work on weak laptops. The trade-off is that small layout mistakes become much easier to see.

Keep the header outside the scrolling rows when you can. That gives you a steady place for labels, sort controls, and resize handles, and it stops the header from rerendering on every scroll tick. If the header and body drift even a few pixels apart, users notice right away.

Lock column widths before you virtualize. If widths change while the user scrolls, the whole table can twitch, and cells no longer line up. Pick widths from the data shape, allow manual resizing, then keep those widths stable while rows enter and leave the DOM.

Cell rendering gets expensive fast

Most table slowdowns come from cells, not rows alone. A row with 12 columns means 12 render paths, 12 style calculations, and often 12 event targets.

That cost grows fast. If 40 rows stay visible and each row has 15 columns, the browser deals with 600 cells at once. Add icons, dropdowns, date formatting, status chips, and inline charts, and scrolling starts to feel rough.

Trim cell renderers where you can. Plain text is cheap. Menus, popovers, and rich previews should open only when someone interacts with a cell, not sit mounted in every visible row.

Sorting, resizing, and pinned columns need extra testing because they change the table's math. A pinned first column often lives in a separate layer from the main scroll area, so both parts need to stay in sync. If they do not, borders shake and hover states look wrong.

A short test pass catches most table bugs:

  • Scroll deep into the table, then sort a column
  • Resize a few columns to very narrow and very wide sizes
  • Pin and unpin a column while scrolling sideways
  • Tab through cells, select text, and copy a value

Focus and selection often fail in subtle ways. Virtualization reuses DOM nodes, so focus can jump to the wrong cell if row keys change, and text selection can break if a cell rerenders at the wrong moment. If someone copies an invoice number, drags to select part of a name, or tabs into an editable cell, the table should still feel steady.

What changes in trees

A tree is harder than a plain list because every row depends on depth and open state. With React tree virtualization, you stop rendering the whole nested structure. You render a flat list of only the nodes people can see right now.

That sounds small, but it changes the whole approach. If a folder is closed, its children should not exist in the rendered list at all. If a folder opens, add only that branch to the flat list instead of rebuilding every visible row from scratch.

Keep the visible slice small

Think of a file browser with 20,000 items spread across many folders. Most people open a few branches, scan them, then move on. Windowing helps most when you flatten just the open paths and keep hidden branches out of the active list.

A full rebuild on every expand or collapse can still feel slow on a weak laptop. A better pattern is to keep a map of node ids, depth, and parent state, then update only the affected range. Stable ids matter here. They help React reuse rows instead of throwing them away and drawing them again.

Indentation also needs care. Users read tree depth from spacing, so each row should keep clear left padding or a guide line that matches its level. Do not fake depth in a way that changes when rows recycle, or the tree will look like it jumps while scrolling.

Input and reading order

Keyboard use matters more in trees than in simple lists. People expect arrow keys to open, close, and move between items in a predictable order. The DOM order should match the visual order, or screen readers will sound wrong even if the tree looks fine.

A few checks catch most problems:

  • Arrow right opens a closed parent or moves to its first child
  • Arrow left closes an open parent or moves to its parent
  • Tab focus stays on the active item, not every row
  • Expanded state and level stay correct when rows recycle
  • Wrapped labels keep the right height after scrolling away and back

Wrapped labels create one more issue: row height. If some node names span two lines, cache those heights after the first measurement. Without a cache, rows shift during scroll, and the tree feels shaky. That is the sort of bug users notice right away, even if they cannot name it.

Search results on an old laptop

Make Trees Feel Stable
Get help with flat tree models, row height issues, and keyboard behavior.

Search is where long lists start to hurt. Someone types three letters, gets 8,000 matches, and expects the page to keep up while they keep typing. On a weak laptop, the slowdown often starts when the app tries to mount every result row at once.

The problem gets worse when each row carries extra UI. One result may include tags, a price, a status chip, a thumbnail, and a couple of actions. Multiply that by thousands, and React spends too much time building DOM nodes that are still far below the fold.

Windowing cuts that work down to the part people can actually see. Instead of rendering the full result set, it keeps a small slice on screen plus a little buffer above and below. The list still feels complete, but the browser paints maybe 30 rows instead of 8,000.

That change helps while people type, not just while they scroll. React updates fewer nodes after each query change, so the input stays responsive. On an old laptop, that difference is easy to feel.

You still need simple rows. Virtualization will not save a result item that is packed with heavy markup, nested popovers, and large images.

  • Show only the fields people scan first
  • Cap visible tags and hide the rest behind "more"
  • Keep thumbnails small, or skip them
  • Move extra actions into a details panel

Debounced search helps too. Waiting about 150 to 250 ms before running a new query can feel smoother than firing a search on every keystroke. The page does less work, and the user sees fewer flickers while filters and counts update.

A good search row is usually boring in the best way. Name, price, status, and a few tags are enough for the list view. Save the dense layout for the item page or side panel, and the search experience stays fast even on older hardware.

Mistakes that make scrolling janky

A virtualized list can still feel bad if the rows keep changing after React paints them. The most common problem is height. If a table row grows when an avatar loads, a badge wraps, or a tree node opens with unknown content, the scroll position shifts and users feel a jump.

Pick a fixed row height when you can. If you cannot, measure rows early and update size in a controlled way. React virtualization libraries work best when the list knows where each item starts before the user flicks the wheel.

Using the array index as the React key causes a different mess. Sort a table, filter search results, or insert one item at the top, and React can reuse the wrong row. Checkboxes move, expanded branches stick to the wrong tree node, and the list feels broken. Use a stable id from your data instead.

The row itself often does too much. A long results list should not mount charts, rich editors, or a pile of tooltips in every visible item. Even with windowing in React, each visible row still renders, handles events, and runs effects. On a weak laptop, a small viewport can get heavy fast.

A few habits prevent most jank:

  • Keep row markup small and delay expensive widgets until the user clicks.
  • Load the next chunk of data before the user reaches the edge of the window.
  • Debounce filtering so the list does not rebuild on every keystroke.
  • Memoize row components when their props rarely change.

Late data loading creates another ugly failure: blank gaps. Placeholder rows usually feel better than empty space, especially in virtualized tables React apps where people expect the grid to stay steady.

Search often causes the worst stalls because people type fast. If every letter triggers filtering, sorting, match highlighting, and rerendering, the UI locks up. Delay that work a little, and keep the input responsive first. Smooth typing matters as much as smooth scrolling.

Quick checks before you ship

Get Fractional CTO Support
Use Oleg's product and architecture experience to turn one slow screen into a repeatable process.

A list can feel perfect on your laptop and still fall apart on an old office machine. Test the rough cases before release, because React virtualization libraries only solve part of the problem. Heavy row content, bad measurements, and too many rerenders can still make the UI feel slow.

Use one weak device if you can. An older Windows laptop with lots of browser tabs open is often enough to expose problems fast.

  • Scroll from top to bottom, then back up. Do it slowly, then flick the wheel or trackpad hard. Watch for jumps, rows that pop in late, sticky headers that lag, or a table that loses its place.
  • Resize the window a few times. Make it narrow, wide, short, and tall. Blank gaps, overlapping rows, or a tree that collapses into empty space usually mean your size math is off.
  • Open row menus, select items with the keyboard, and keep moving through the list. The focus ring should stay visible, and the active item should not disappear just because the virtualizer recycled that part of the screen.
  • Force every state you support. Show loading, empty, and error views inside the same container where real results appear. If the layout jumps between states, people will feel it even when they cannot explain why.
  • Type into search while the list scrolls. Open your browser task manager or performance tools and watch CPU use. If one short query spikes the CPU and the input lags, your filtering, highlighting, or row rendering still does too much work.

One small example: a search page may scroll smoothly until you add match highlighting, avatars, and a result count per row. Then typing one letter can lock up an old laptop for half a second. That usually means the list window is fine, but each visible row is still too expensive.

If you find problems, fix the most obvious one first. Memoize row components, reduce work during typing, and keep row heights predictable. A fast list should still feel calm when the device is tired.

What to do next

Start with the screen that hurts the most. If one view makes a weak laptop stutter, fix that first instead of trying to virtualize every list in the app at once.

A simple order works well:

  • Pick the slowest screen, such as a search results page with hundreds of rows.
  • Measure it before the change. Check render time, scroll smoothness, and CPU spikes.
  • Add windowing to that one view and measure again with the same data.
  • Ship it to a small group first, then watch for bugs around selection, keyboard input, and loading states.
  • Repeat only after the first rollout feels stable.

This keeps the work small and makes the result easy to judge. If the page drops from a choppy 2 seconds of heavy rendering to a smooth first scroll, your team will see the gain right away.

Do not treat virtualization as a cure for every slow screen. Sometimes list slowness points to a deeper issue: too many re-renders, heavy row components, expensive filtering on each keystroke, or product decisions that dump too much data on one page. In those cases, windowing helps, but it does not fix the root problem.

That is why before-and-after comparison matters. If a virtualized list still feels bad, the next step is usually a review of rendering patterns, state updates, and page behavior under real data. A short review can save days of guesswork.

If your team needs broader help, Oleg Sotnikov can review architecture, UI performance, and AI-first product work in a practical way. He works as a Fractional CTO and startup advisor, and his background covers both product architecture and lean production systems. That fits teams who want more than a quick patch and need a plan they can keep using after the first fast list goes live.