Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 65 additions & 16 deletions apps/webapp/app/components/primitives/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
Component,
ComponentPropsWithoutRef,
Fragment,
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
Expand All @@ -19,28 +19,77 @@ const MousePositionContext = createContext<MousePosition | undefined>(undefined)
export function MousePositionProvider({ children }: { children: ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<MousePosition | undefined>(undefined);
const lastClient = useRef<{ clientX: number; clientY: number } | null>(null);
const rafId = useRef<number | null>(null);

const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
if (!ref.current) {
setPosition(undefined);
return;
}
const computeFromClient = useCallback((clientX: number, clientY: number) => {
if (!ref.current) {
setPosition(undefined);
return;
}

const { top, left, width, height } = ref.current.getBoundingClientRect();
const x = (e.clientX - left) / width;
const y = (e.clientY - top) / height;
const { top, left, width, height } = ref.current.getBoundingClientRect();
const x = (clientX - left) / width;
const y = (clientY - top) / height;

if (x < 0 || x > 1 || y < 0 || y > 1) {
setPosition(undefined);
return;
}
if (x < 0 || x > 1 || y < 0 || y > 1) {
setPosition(undefined);
return;
}

setPosition({ x, y });
setPosition({ x, y });
}, []);

const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
lastClient.current = { clientX: e.clientX, clientY: e.clientY };
computeFromClient(e.clientX, e.clientY);
},
[ref.current]
[computeFromClient]
);

// Recalculate the relative position when the container resizes or the window/ancestors scroll.
useEffect(() => {
if (!ref.current) return;

const ro = new ResizeObserver(() => {
const lc = lastClient.current;
if (lc) computeFromClient(lc.clientX, lc.clientY);
});
ro.observe(ref.current);

const onRecalc = () => {
const lc = lastClient.current;
if (lc) computeFromClient(lc.clientX, lc.clientY);
};

window.addEventListener("resize", onRecalc);
// Use capture to catch scroll on any ancestor that impacts bounding rect
window.addEventListener("scroll", onRecalc, true);

return () => {
ro.disconnect();
window.removeEventListener("resize", onRecalc);
window.removeEventListener("scroll", onRecalc, true);
};
}, [computeFromClient]);

useEffect(() => {
if (position === undefined || !lastClient.current) return;

const tick = () => {
const lc = lastClient.current;
if (lc) computeFromClient(lc.clientX, lc.clientY);
rafId.current = requestAnimationFrame(tick);
};

rafId.current = requestAnimationFrame(tick);
return () => {
if (rafId.current !== null) cancelAnimationFrame(rafId.current);
rafId.current = null;
};
}, [position, computeFromClient]);
Comment on lines +77 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Optimize the animation frame loop to reduce performance overhead.

The continuous RAF loop runs at ~60fps whenever the mouse is hovering, repeatedly calling getBoundingClientRect() even when the container is static. This forced layout operation on every frame can impact performance, especially with multiple timeline instances or on lower-end devices.

Consider these optimizations:

  1. Add a transition/animation detector to only run the RAF loop when the container is actually animating
  2. Use a throttle or debounce mechanism to reduce frequency
  3. Add a flag to control when continuous updates are needed

Additionally, the effect's dependency on position causes the RAF loop to restart whenever position updates during animations, which is inefficient.

Example: Only run RAF during transitions

  useEffect(() => {
-   if (position === undefined || !lastClient.current) return;
+   if (position === undefined || !lastClient.current || !ref.current) return;
+   
+   // Check if container or ancestors are transitioning/animating
+   const isAnimating = () => {
+     if (!ref.current) return false;
+     const styles = window.getComputedStyle(ref.current);
+     // Check for ongoing transitions or animations
+     return styles.transition !== 'none' || styles.animation !== 'none';
+   };

    const tick = () => {
      const lc = lastClient.current;
-     if (lc) computeFromClient(lc.clientX, lc.clientY);
-     rafId.current = requestAnimationFrame(tick);
+     if (lc) {
+       computeFromClient(lc.clientX, lc.clientY);
+       // Only continue RAF if actively animating
+       if (isAnimating()) {
+         rafId.current = requestAnimationFrame(tick);
+       } else {
+         rafId.current = null;
+       }
+     }
    };

    rafId.current = requestAnimationFrame(tick);
    return () => {
      if (rafId.current !== null) cancelAnimationFrame(rafId.current);
      rafId.current = null;
    };
  }, [position, computeFromClient]);

Or consider removing the RAF loop entirely and relying on the ResizeObserver and scroll/resize listeners, which should handle most layout changes adequately.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/webapp/app/components/primitives/Timeline.tsx around lines 77–91, the
current RAF loop runs continuously while hovering and forces layout each frame;
instead start/stop RAF only when the container is actually animating or needs
continuous updates: add a ref/boolean (e.g., needsContinuousUpdate or
isAnimating) and detect animations/transitions via element.getAnimations() or
getComputedStyle(elt).transitionDuration > 0 (and listen for
animationend/transitionend to clear the flag), run requestAnimationFrame only
when that flag is true, cancel it when the flag clears, and replace the
RAF-for-everything approach with ResizeObserver and scroll/resize listeners (or
a throttled handler) to handle static layout changes; also avoid restarting the
effect on every mouse move by removing position from the dependency array and
using refs for computeFromClient, and add a small throttle/debounce on
computeFromClient to reduce frequency while preserving responsiveness.


return (
<div
ref={ref}
Expand Down
Loading