From 5d29f05248ad267c517e9a6a37a69cc47f46fc7d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:02:30 +0000 Subject: [PATCH] perf: Optimize GradientBlinds pointer move handler - Removed synchronous `getBoundingClientRect` call from `pointermove` handler - Implemented caching of canvas dimensions and scroll position using `ResizeObserver` - Added logic to compensate for scroll delta in pointer position calculation - Added regression test to verify `getBoundingClientRect` is not called during pointer interaction - Fixed `ogl` mock in tests to include `Geometry` --- .../effects/GradientBlinds.test.tsx | 32 +++++++++++++++++++ src/components/effects/GradientBlinds.tsx | 24 ++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/components/effects/GradientBlinds.test.tsx b/src/components/effects/GradientBlinds.test.tsx index e29a176..d75d181 100644 --- a/src/components/effects/GradientBlinds.test.tsx +++ b/src/components/effects/GradientBlinds.test.tsx @@ -22,6 +22,9 @@ vi.mock('ogl', () => { Mesh: class { remove() {} }, + Geometry: class { + remove() {} + }, Triangle: class { remove() {} }, @@ -129,4 +132,33 @@ describe('GradientBlinds', () => { unmount(); expect(disconnectSpy).toHaveBeenCalled(); }); + + it('minimizes getBoundingClientRect calls during pointer move', () => { + const { unmount } = render(); + + // Spy on getBoundingClientRect + // Note: In jsdom, canvas is an HTMLCanvasElement which inherits from HTMLElement + const spy = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect'); + + // Trigger pointer move to clear any initial calls or verify baseline + // The initial render calls resize(), which calls getBoundingClientRect on container + + // Clear spy history from initial render + spy.mockClear(); + + act(() => { + const event = new PointerEvent('pointermove', { + clientX: 200, + clientY: 200, + bubbles: true, + }); + window.dispatchEvent(event); + }); + + // EXPECTATION: It should NOT be called. + // This will fail currently. + expect(spy).not.toHaveBeenCalled(); + + unmount(); + }); }); diff --git a/src/components/effects/GradientBlinds.tsx b/src/components/effects/GradientBlinds.tsx index 13ca4eb..9b44f1f 100644 --- a/src/components/effects/GradientBlinds.tsx +++ b/src/components/effects/GradientBlinds.tsx @@ -66,6 +66,8 @@ const GradientBlinds: React.FC = ({ const mouseTargetRef = useRef<[number, number]>([0, 0]); const lastTimeRef = useRef(0); const firstResizeRef = useRef(true); + const rectRef = useRef(null); + const scrollPosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); useEffect(() => { const container = containerRef.current; @@ -277,6 +279,10 @@ void main() { const resize = () => { const rect = container.getBoundingClientRect(); + rectRef.current = rect; + if (typeof window !== 'undefined') { + scrollPosRef.current = { x: window.scrollX, y: window.scrollY }; + } renderer.setSize(rect.width, rect.height); uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1]; @@ -303,10 +309,22 @@ void main() { ro.observe(container); const onPointerMove = (e: PointerEvent) => { - const rect = canvas.getBoundingClientRect(); const scale = (renderer as unknown as { dpr?: number }).dpr || 1; - const x = (e.clientX - rect.left) * scale; - const y = (rect.height - (e.clientY - rect.top)) * scale; + let x, y; + + if (rectRef.current) { + const dx = window.scrollX - scrollPosRef.current.x; + const dy = window.scrollY - scrollPosRef.current.y; + const rectLeft = rectRef.current.left - dx; + const rectTop = rectRef.current.top - dy; + x = (e.clientX - rectLeft) * scale; + y = (rectRef.current.height - (e.clientY - rectTop)) * scale; + } else { + const rect = canvas.getBoundingClientRect(); + x = (e.clientX - rect.left) * scale; + y = (rect.height - (e.clientY - rect.top)) * scale; + } + mouseTargetRef.current = [x, y]; if (mouseDampening <= 0) { uniforms.iMouse.value = [x, y]; -- 2.49.1