Whenever the timeframe changes, a recalculation of all the item's width and position must be performed.
When performing operations that require a high amount of sequential changes, like zooming in or panning, the resulted re-renders might cause stuttering and frame-drops.
This "frequent change" problem is quite common in reactive frontend applications, and there are multiple solutions to it.
As you are in full control of the timeframe state, you can apply a chosen mechanism of debouncing to the timeframe state.
This will reduce the amount changes registered in the timeline, and in return reduce the amount of renders required when zooming an panning, resulting in a great improvement in performance.
Here are some common solutions:
If you use React 18+, you should make use of the useDeferredValue API.
useDeferredValue is a React Hook that lets you defer updating a part of the UI.
Code Example
functionApp() {const [timeframe,setTimeframe] =useState(DEFAULT_TIMEFRAME);constdebouncedTimeframe=useDeferredValue(timeframe);...return ( <TimelineContextonDragEnd={onDragEnd}onResizeEnd={onResizeEnd}timeframe={debouncedTimeframe} // provide the debounced timeframeonTimeframeChanged={setTimeframe} > <Timelineitems={items} rows={rows} /> </TimelineContext> )
functionItem(props:ItemProps) {...conststyle:CSSProperties= {...itemStyle, transition:'left .2s linear, width .2s linear',// You can a CSS transition to make the debounce feel smoother }return ( <divref={setNodeRef} style={style} {...listeners} {...attributes}> ... </div> )}
Another common solution is throttling.
Throttling limits the rate of function calls. It guarantees that a function is only executed once within a set time interval. If the function is called multiple times during that interval, only the first call is executed. Subsequent calls are ignored until the interval has elapsed.
Code Example
functionApp() {const [timeframe,setTimeframe] =useState(DEFAULT_TIMEFRAME);constdebouncedTimeframe=useThrottle(timeframe,300);...return ( <TimelineContextonDragEnd={onDragEnd}onResizeEnd={onResizeEnd}timeframe={debouncedTimeframe} // provide the debounced timeframeonTimeframeChanged={setTimeframe} > <Timelineitems={items} rows={rows} /> </TimelineContext> )
functionItem(props:ItemProps) {...conststyle:CSSProperties= {...itemStyle, transition:'left .2s linear, width .2s linear',// You can a CSS transition to make the debounce feel smoother }return ( <divref={setNodeRef} style={style} {...listeners} {...attributes}> ... </div> )}
The most common way to solve this is using debounce.
Debouncing prevents extra activations or slow functions from triggering too often. It will wait for a given interval to elapse since the last change, and only then apply it.
Code Example
functionApp() {const [timeframe,setTimeframe] =useState(DEFAULT_TIMEFRAME);constdebouncedTimeframe=useDebounce(timeframe,300)...return ( <TimelineContextonDragEnd={onDragEnd}onResizeEnd={onResizeEnd}timeframe={debouncedTimeframe} // provide the debounced timeframeonTimeframeChanged={setTimeframe} > <Timelineitems={items} rows={rows} /> </TimelineContext> )
functionItem(props:ItemProps) {...conststyle:CSSProperties= {...itemStyle, transition:'left .2s linear, width .2s linear',// You can a CSS transition to make the debounce feel smoother }return ( <divref={setNodeRef} style={style} {...listeners} {...attributes}> ... </div> )}
🧠 You can still make use of the un-debounced state to render selected components in real-time. For example, you can render the time-axis using the un-debounced state, and render the timeline using the debounced state.
Play around with the live demo, and watch how the timeaxis and the timeframe move asynchronously🔥
Live Example
In this example, You can play around with difference modes and feel the difference between them.