Performance
The targets 1.0 is measured against, what the calendar does to meet them, and what your app can do with large data.
The 1.0 targets
1.0 is measured against four scenarios, on a laptop and with the CPU slowed down four times (as Chrome's DevTools do), and against Interaction to Next Paint for three interactions. Each figure is the median of five runs; the last column is the latest measurement (October 2026, Chrome, on a development laptop that was running other work, so expect a few milliseconds either way):
| Scenario | Laptop | 4× CPU throttle | Measured (laptop / 4×) |
|---|---|---|---|
| Normalize and index 10,000 events (cold) | ≤ 40 ms | ≤ 200 ms | 15 / 62 ms |
| Change one event in a set of 10,000 | ≤ 1 ms | ≤ 5 ms | 0.9 / 3.6 ms |
| Navigate a week with 500 visible events | ≤ 25 ms | ≤ 100 ms | 62 / 230–253 ms |
| Show a month with 2,000 events | ≤ 40 ms | ≤ 150 ms | 34 / 109–120 ms |
INP stays under 100 ms at 4× throttle for a click on an event, a selection and a keyboard move (the worst of five runs measured about 24 ms for the selection and 48 ms for the keyboard move). The week and month scenarios use the editable block, every event with its actions, which is the heaviest configuration, from the click on Next to the first frame after the new range renders.
Not met yet: navigating a week with 500 events
The week scenario misses its budget. Most of its time is the browser's style and layout work for the roughly 4,600 elements of 500 editable events (about 33 ms of the laptop's figure, part of it the docs site's own CSS on the measuring page) and React rendering them (about 19 ms); the calendar's own model takes about 2.4 ms. Before the 1.0 release the maintainers decide how 1.0 measures this scenario or what its budget is; until then it is reported, not enforced. The other scenarios and every INP target are met.
What the calendar does
- Numbers inside. Dates are epoch milliseconds and day numbers internally;
Temporalobjects are created only when your code reads one (event.start). - Work per change, not per render. Events are normalized per input object and indexed by time; the view model keeps the identity of everything that did not change, so memoized parts skip rendering. A minute tick of the clock re-packs nothing.
- Only the touched parts re-render. Event elements and their actions buttons share one store subscription per calendar, keyed by event, so a change re-renders the events it changed; a drop re-renders the days it leaves and lands on, not the whole week or month. A hidden actions button is a single element.
- No re-render per pointer move. A drag updates the store only when the snapped target changes; the drag preview follows the pointer outside React.
- Only what you use is bundled. A read-only calendar ships no selection, editing or drop code;
each engine is passed in (
<CalendarSelection>,<CalendarEditing>,<CalendarExternalDrops>), and the recurrence engine only withseries. - Series cost what is visible. Recurring series are expanded over the visible range only, the expansion is cached, and the neighbouring ranges are prefetched while the browser is idle. On a laptop, 500 weekly series (about 5,000 occurrences in a month) take about 30 ms the first time and 11–13 ms for the next month after its prefetch, most of it spent building that month's occurrences.
What your app can do
- Keep identities stable. Pass the same events array, accessor, sources, views, formats,
strings and engines while they are unchanged: define them at module scope or memoize them. A new
eventsarray is new data; a new engine ends an interaction in progress. - Load by range. Event sources (or a query keyed by
onRangeChange) load the visible range only;window: 'week'or'month'saves requests while users move inside it. - Pass your objects as they are. Accessor mode (
eventItemsandeventAccessor) reads your objects instead of copying them into event inputs. - Keep render functions cheap.
renderEventandclassNamesfunctions run for every event that renders: no heavy work, no new components defined inside them. - Limit what a dense view draws.
maxEvents: 'auto'(the month's default) shows what fits and puts the rest behind "+N more";maxStackdoes the same for overlapping events in a time grid. - Server-render less when weeks are full.
ssrEvents={false}keeps a dense week's events out of the HTML; they render right after hydration.
Lists with thousands of rows and timelines are virtualized from 1.2.