From 1fe7db0e16074ac951d10b24526f1f0adf027c85 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:39:11 +0000 Subject: [PATCH] perf(effects): optimize GradientBlinds loop - Replace immutable refs with mutable objects for pointer tracking to reduce GC pressure - Remove synchronous getBoundingClientRect from animation loop to prevent layout thrashing - Ensure mouse coordinates are updated efficiently without re-allocating arrays Co-authored-by: ragusa-it <196988693+ragusa-it@users.noreply.github.com> --- .jules/bolt.md | 4 ++ src/components/effects/GradientBlinds.tsx | 60 +++++++++++------------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 6872ec9..27808c7 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -8,3 +8,7 @@ ## 2024-05-22 - High-Frequency State Isolation **Learning:** High-frequency state updates (like typing effects) in large parent components (`Hero`) trigger massive unnecessary re-renders of expensive sub-trees (`GradientBlinds`, `Button`). **Action:** Isolate high-frequency state into small, leaf-node components (e.g., `TypedText`) and wrap them in `React.memo` if necessary, keeping the heavy parent static. + +## 2025-01-26 - WebGL Layout Thrashing +**Learning:** WebGL animation loops often contain fallback paths (e.g., `if (!rect) rect = getBoundingClientRect()`) that trigger synchronous layout thrashing every frame if the primary path fails or initializes late. +**Action:** Remove synchronous layout reads from `requestAnimationFrame` loops. Use `ResizeObserver` to cache dimensions and handle the "not ready" state by skipping updates or using safe defaults (0,0) instead of forcing a reflow. diff --git a/src/components/effects/GradientBlinds.tsx b/src/components/effects/GradientBlinds.tsx index 9f3b787..2e72b60 100644 --- a/src/components/effects/GradientBlinds.tsx +++ b/src/components/effects/GradientBlinds.tsx @@ -63,9 +63,10 @@ const GradientBlinds: React.FC = ({ const meshRef = useRef(null); const geometryRef = useRef(null); const rendererRef = useRef(null); - const mouseTargetRef = useRef<[number, number]>([0, 0]); + const mouseTargetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); // Optimization: store raw pointer position (viewport coords) to decouple event handling from calculation - const pointerPosRef = useRef<{ x: number; y: number } | null>(null); + // Use mutable object to avoid GC + const pointerPosRef = useRef<{ x: number; y: number; active: boolean }>({ x: 0, y: 0, active: false }); const isMobileRef = useRef(false); const lastTimeRef = useRef(0); const firstResizeRef = useRef(true); @@ -304,7 +305,7 @@ void main() { const cx = gl.drawingBufferWidth / 2; const cy = gl.drawingBufferHeight / 2; uniforms.iMouse.value = [cx, cy]; - mouseTargetRef.current = [cx, cy]; + mouseTargetRef.current = { x: cx, y: cy }; } }; @@ -318,7 +319,7 @@ void main() { // This prevents the "ghost drift" effect when scrolling with inertia after lifting finger, // because we won't be updating the target based on scroll position in the loop. const scale = (renderer as unknown as { dpr?: number }).dpr || 1; - let x, y; + let x = 0, y = 0; if (rectRef.current) { const dx = window.scrollX - scrollPosRef.current.x; @@ -327,15 +328,16 @@ void main() { 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]; - pointerPosRef.current = null; // Ensure loop doesn't override + // Fallback: do nothing if rect is not ready yet to avoid sync layout thrashing + + mouseTargetRef.current.x = x; + mouseTargetRef.current.y = y; + pointerPosRef.current.active = false; // Ensure loop doesn't override } else { - pointerPosRef.current = { x: e.clientX, y: e.clientY }; + pointerPosRef.current.x = e.clientX; + pointerPosRef.current.y = e.clientY; + pointerPosRef.current.active = true; } }; @@ -344,24 +346,19 @@ void main() { uniforms.iTime.value = t * 0.001; // Update target based on pointer position and scroll offset - if (pointerPosRef.current) { + if (pointerPosRef.current.active && rectRef.current) { const scale = (renderer as unknown as { dpr?: number }).dpr || 1; - 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 = (pointerPosRef.current.x - rectLeft) * scale; - y = (rectRef.current.height - (pointerPosRef.current.y - rectTop)) * scale; - } else { - // Fallback if rectRef missing - const rect = canvas.getBoundingClientRect(); - x = (pointerPosRef.current.x - rect.left) * scale; - y = (rect.height - (pointerPosRef.current.y - rect.top)) * scale; - } - mouseTargetRef.current = [x, y]; + 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; + + const x = (pointerPosRef.current.x - rectLeft) * scale; + const y = (rectRef.current.height - (pointerPosRef.current.y - rectTop)) * scale; + + mouseTargetRef.current.x = x; + mouseTargetRef.current.y = y; } if (mouseDampening > 0) { @@ -373,11 +370,12 @@ void main() { if (factor > 1) factor = 1; const target = mouseTargetRef.current; const cur = uniforms.iMouse.value; - cur[0] += (target[0] - cur[0]) * factor; - cur[1] += (target[1] - cur[1]) * factor; + cur[0] += (target.x - cur[0]) * factor; + cur[1] += (target.y - cur[1]) * factor; } else { - if (pointerPosRef.current || isMobileRef.current) { - uniforms.iMouse.value = mouseTargetRef.current; + if (pointerPosRef.current.active || isMobileRef.current) { + uniforms.iMouse.value[0] = mouseTargetRef.current.x; + uniforms.iMouse.value[1] = mouseTargetRef.current.y; } lastTimeRef.current = t; } -- 2.49.1