dnd-timeline
v1
v1
  • 🎊dnd-timeline
  • Guide
    • Introduction
    • Installation
  • Documentation
    • TimelineContext
      • useTimelineContext
    • Row
      • useRow
      • Subrow
    • Item
      • useItem
    • Zoom & Pan
      • Performance
  • Examples
Powered by GitBook
On this page
  1. Documentation
  2. Zoom & Pan

Performance

PreviousZoom & PanNextExamples

Last updated 10 months ago

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 API.

useDeferredValue is a React Hook that lets you defer updating a part of the UI.

Code Example

function App() {
  const [timeframe, setTimeframe] = useState(DEFAULT_TIMEFRAME);
  const debouncedTimeframe = useDeferredValue(timeframe);
  
  ...
  
  return (
    <TimelineContext
        onDragEnd={onDragEnd}
        onResizeEnd={onResizeEnd}
        timeframe={debouncedTimeframe} // provide the debounced timeframe
        onTimeframeChanged={setTimeframe}
    >
      <Timeline items={items} rows={rows} />
    </TimelineContext>
  )
function Item(props: ItemProps) {
  ...
  const style: CSSProperties = {
    ...itemStyle,
    transition: 'left .2s linear, width .2s linear', 
    // You can a CSS transition to make the debounce feel smoother
  }

  return (
    <div ref={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

function App() {
  const [timeframe, setTimeframe] = useState(DEFAULT_TIMEFRAME);
  const debouncedTimeframe = useThrottle(timeframe, 300);
  
  ...
  
  return (
    <TimelineContext
        onDragEnd={onDragEnd}
        onResizeEnd={onResizeEnd}
        timeframe={debouncedTimeframe} // provide the debounced timeframe
        onTimeframeChanged={setTimeframe}
    >
      <Timeline items={items} rows={rows} />
    </TimelineContext>
  )
function Item(props: ItemProps) {
  ...
  const style: CSSProperties = {
    ...itemStyle,
    transition: 'left .2s linear, width .2s linear', 
    // You can a CSS transition to make the debounce feel smoother
  }

  return (
    <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
    ...
    </div>
  )
}
export function useThrottle<T>(value: T, interval = 500) {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastUpdated = useRef<number | null>(null);

  useEffect(() => {
    const now = Date.now();

    if (lastUpdated.current && now >= lastUpdated.current + interval) {
      lastUpdated.current = now;
      setThrottledValue(value);
    } else {
      const id = window.setTimeout(() => {
        lastUpdated.current = now;
        setThrottledValue(value);
      }, interval);

      return () => {
        window.clearTimeout(id)
      };
    }
  }, [value, interval]);

  return throttledValue;
}

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

function App() {
  const [timeframe, setTimeframe] = useState(DEFAULT_TIMEFRAME);
  const debouncedTimeframe = useDebounce(timeframe, 300)
  
  ...
  
  return (
    <TimelineContext
        onDragEnd={onDragEnd}
        onResizeEnd={onResizeEnd}
        timeframe={debouncedTimeframe} // provide the debounced timeframe
        onTimeframeChanged={setTimeframe}
    >
      <Timeline items={items} rows={rows} />
    </TimelineContext>
  )
function Item(props: ItemProps) {
  ...
  const style: CSSProperties = {
    ...itemStyle,
    transition: 'left .2s linear, width .2s linear', 
    // You can a CSS transition to make the debounce feel smoother
  }

  return (
    <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
    ...
    </div>
  )
}
export function useDebounce<T>(value: T, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

🧠 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.

useDeferredValue
Click here to see the full code
dnd-timeline | Basic
Logo