๐ฅ
ExclusiveAn exclusive new experience โ React Native support is on its way to Planby!
Building a High-Performance React Scheduler with Virtualization
How virtual rendering and windowing make it possible to display 10,000+ events in a React timeline scheduler without sacrificing 60fps scroll performance.
Karol Kozer
ยท
February 20, 2026
ยท
8 min read
Every React timeline scheduler faces the same problem: as the number of events grows, the DOM grows too โ and rendering thousands of absolutely positioned elements simultaneously kills scroll performance. The standard fix is virtualization (also called windowing). Here is how it works, why it is harder to apply to a 2D timeline than to a vertical list, and how Planby implements it to maintain 60fps with 10,000+ events.
Why naive rendering fails at scale
A React timeline scheduler maps events to absolutely positioned DOM elements inside a scrollable container. Each event needs a left, width, and top value calculated from the event's start time, duration, and resource row.
With 100 events this is trivial โ 100 DOM nodes, each painted once. With 10,000 events the browser must:
1.
Mount 10,000 React components on initial render
2.
Run layout (reflow) on 10,000 absolutely positioned elements
3.
Repaint the visible viewport on every scroll event
4.
Keep all 10,000 nodes in memory even when they are far off-screen
The result: janky scrolling, high memory usage, and sometimes a completely unresponsive tab. This is not a React problem โ it is a DOM problem. The browser simply cannot efficiently manage that many painted nodes.
Virtual rendering: only mount what is visible
The solution is to render only the events that intersect the current viewport. When the user scrolls, unmount events that have left the viewport and mount the newly visible ones. From the browser's perspective, there are never more than ~50 DOM nodes on screen โ regardless of how many events exist in the dataset.
Libraries like react-window and react-virtualized implement this for simple vertical lists. A timeline scheduler, however, is a 2D surface โ it scrolls both horizontally (time axis) and vertically (resource/row axis). Naive list virtualization only solves one axis.
Key insight
A timeline scheduler needs bidirectional windowing: events are filtered by both the horizontal scroll position (visible time range) and the vertical scroll position (visible rows). Only events that are visible on both axes are mounted into the DOM.
DOM node count: virtualized vs non-virtualized
The clearest way to see the impact of virtualization is to compare DOM node counts at different dataset sizes. With a virtualized scheduler, the count stays constant because it only depends on how many events fit in the current viewport โ not on total dataset size.
| Events in dataset | Non-virtualized DOM nodes | Virtualized DOM nodes (Planby) |
|---|---|---|
| 100 | ~100 | ~40โ60 |
| 500 | ~500 | ~40โ60 |
| 1,000 | ~1,000 | ~40โ60 |
| 5,000 | ~5,000 | ~40โ60 |
| 10,000 | ~10,000 | ~40โ60 |
DOM node count with Planby is determined by viewport dimensions (how many events fit on screen), not by total dataset size.
How Planby implements bidirectional windowing
Planby's useEpg hook calculates three things on every scroll event:
1. Visible time range
Based on the current horizontal scroll position and the container width, Planby computes the start and end timestamps of the visible time window. Only events whose time range overlaps this window are candidates for rendering.
2. Visible row range
Based on the vertical scroll position and container height, Planby determines which resource rows are currently visible. Only events in those rows pass the second filter.
3. Intersection
Events that pass both filters are mounted. Everything else stays unmounted. This is O(n) in the total event count but the mounted count stays near constant (viewport-bound).
The hook returns a filtered epg array containing only the events that should be rendered. The Layout component maps over this array and renders the visible events. When the user scrolls, React reconciles the diff โ new events appear, out-of-view events disappear.
60fps scroll performance: the rendering budget
To maintain 60fps, each frame must complete in under 16.67ms. With 10,000 events mounted naively, a single scroll event triggers reflows across thousands of DOM nodes โ easily exceeding that budget and producing visible jank.
With virtualization, a scroll event only needs to:
1.
Recalculate the visible time window (pure JS, microseconds)
2.
Recalculate the visible row window (pure JS, microseconds)
3.
Compute the intersection (array filter, O(n) but fast for n=10k)
4.
Reconcile a ~50-node React tree (fast, bounded)
The total cost per scroll event stays well within the 16ms budget. Users see smooth, responsive scrolling regardless of how large the underlying dataset is.
Beyond events: virtualizing rows too
A scheduler with 500 resources (rows) has the same problem on the vertical axis. Planby handles this by also virtualizing the row (channel) list. Only the rows that are currently visible in the vertical viewport are rendered โ the sidebar labels and the event rows together.
This matters for use cases like large employee shift schedulers, hotel room booking systems, or production line planners where there may be hundreds or thousands of resource rows.
Why most React schedulers do not virtualize
Bidirectional virtualization on a 2D time-axis is significantly more complex than a simple vertical list. Most React scheduler libraries skip it for this reason โ FullCalendar, React Big Calendar, and DHTMLX Scheduler all render their full event datasets into the DOM. This works acceptably for small datasets (under a few hundred events) but degrades predictably as dataset size grows.
FullCalendar note
FullCalendar's resource timeline view (their closest equivalent to a horizontal scheduler) requires a $1,099/year Premium license and has no virtualization. With large event counts, its performance degrades measurably.
Summary
Virtualization is not optional for production-grade React timeline schedulers with real-world data volumes. The key points:
โ
Naive rendering puts all events into the DOM โ performance degrades linearly with event count
โ
Virtual rendering keeps DOM node count constant (~40โ60) regardless of dataset size
โ
A timeline scheduler needs bidirectional windowing (horizontal time axis + vertical row axis)
โ
Planby implements both axes simultaneously in the useEpg hook
โ
60fps scroll is achievable even at 10,000+ events when the DOM stays small
If you are evaluating React scheduler libraries for a data-heavy use case, virtual rendering is the single most important capability to check for. Without it, any library will hit a wall as your data grows.
Try Planby โ the virtualized React scheduler
Free tier available. PRO license is one-time, no annual renewal.
npm install @nessprim/planbyplanby
The most powerful React scheduler & timeline component for building modern scheduling interfaces.
ยฉ 2026 Nessprim Karol Kozer. All rights reserved.
Built with โค๏ธ for developers