Performance
Where Time does its work, and how to keep that work small.
Time does its work in two steps, and caches each one:
- Reading events.
Rootruns your records through the adapter into a snapshot. This happens again only when the records change. - Projecting the view.
Rootcalls the active view'sprojectwith the snapshot and the visible range. This happens again only when the snapshot, range, options, resources, environment, or today's date change.
Everything else is a normal React render. These habits keep both steps rare and cheap.
Replace records, don't recreate them
Root compares events item by item. A new array with the same record objects in the same order
counts as unchanged, so you can filter or map inline, as in
events={meetings.filter((meeting) => !meeting.cancelled)}.
When some records change, Time reads only the new objects. So update records immutably: replace the changed record and keep the others as they are.
Add a revision to your adapter
When you refetch, your data layer usually creates new objects for every record, even unchanged ones.
getRevision lets Time skip those. Return anything that changes whenever the record changes, such as
an updatedAt timestamp or a version number:
const bookingAdapter = defineEventAdapter<Booking>()({
getId: (booking) => booking.id,
getTitle: (booking) => booking.summary,
getInterval: (booking) => ({ kind: "timed", start: booking.startsAt, end: booking.endsAt }),
getResourceIds: (booking) => booking.roomIds,
getRevision: (booking) => booking.updatedAt,
});Records with the same ID and revision as last time reuse their earlier result. The revision must change whenever anything the adapter reads changes; otherwise Time keeps showing the old title or time.
Do the heavy work in project
A view's project runs only when its inputs change, while its Renderer re-renders like any
component. Put filtering, sorting, and layout in project, and keep the Renderer to drawing.
Inside project, ask the snapshot for the visible range instead of looping over every event:
project: ({ snapshot, instantRange }) => snapshot.queryTimed(instantRange),Queries use an interval index, so they don't scan every event. Each snapshot also remembers the results of its last 128 distinct queries, so asking again for the same range returns the same array.
Keep functions in options stable
Root compares locale, weekRules, now, controlled values, and each view's options by value,
so inline objects are fine. Functions are different: a new function is a new value. A
getEventColor written inline in viewOptions makes the view project again on every render of the
component that renders Root. Define it once instead:
// At module scope, outside the component that renders Root:
const getEventColor = (meeting: Meeting) => (meeting.title === "Standup" ? "green" : "blue");
// Then, on Root:
viewOptions={{ agenda: { getEventColor } }}The same goes for a clock object. Change handlers and onDiagnosticsChange can be inline: Root
reads them when it needs them.
Subscribe to what you use
useCalendarState() and useCalendar() re-render their component whenever any state changes. For a
component that needs one value, use a selector:
function ViewName() {
const view = Calendar.useCalendarSelector((state) => state.view);
return <span>{view}</span>;
}The component re-renders only when the selected value changes. If your selector builds a new object
each time, pass isEqual so equal results don't count as changes:
const position = Calendar.useCalendarSelector(
(state) => ({ view: state.view, anchorDate: state.anchorDate }),
{ isEqual: (a, b) => a.view === b.view && a.anchorDate.equals(b.anchorDate) },
);Last updated on