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>
This commit is contained in:
google-labs-jules[bot]
2026-02-04 01:39:11 +00:00
parent 2587b9dd29
commit 1fe7db0e16
2 changed files with 33 additions and 31 deletions

View File

@@ -8,3 +8,7 @@
## 2024-05-22 - High-Frequency State Isolation ## 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`). **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. **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.

View File

@@ -63,9 +63,10 @@ const GradientBlinds: React.FC<GradientBlindsProps> = ({
const meshRef = useRef<Mesh | null>(null); const meshRef = useRef<Mesh | null>(null);
const geometryRef = useRef<Geometry | null>(null); const geometryRef = useRef<Geometry | null>(null);
const rendererRef = useRef<Renderer | null>(null); const rendererRef = useRef<Renderer | null>(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 // 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<boolean>(false); const isMobileRef = useRef<boolean>(false);
const lastTimeRef = useRef<number>(0); const lastTimeRef = useRef<number>(0);
const firstResizeRef = useRef<boolean>(true); const firstResizeRef = useRef<boolean>(true);
@@ -304,7 +305,7 @@ void main() {
const cx = gl.drawingBufferWidth / 2; const cx = gl.drawingBufferWidth / 2;
const cy = gl.drawingBufferHeight / 2; const cy = gl.drawingBufferHeight / 2;
uniforms.iMouse.value = [cx, cy]; 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, // 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. // 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; const scale = (renderer as unknown as { dpr?: number }).dpr || 1;
let x, y; let x = 0, y = 0;
if (rectRef.current) { if (rectRef.current) {
const dx = window.scrollX - scrollPosRef.current.x; const dx = window.scrollX - scrollPosRef.current.x;
@@ -327,15 +328,16 @@ void main() {
const rectTop = rectRef.current.top - dy; const rectTop = rectRef.current.top - dy;
x = (e.clientX - rectLeft) * scale; x = (e.clientX - rectLeft) * scale;
y = (rectRef.current.height - (e.clientY - rectTop)) * 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]; // Fallback: do nothing if rect is not ready yet to avoid sync layout thrashing
pointerPosRef.current = null; // Ensure loop doesn't override
mouseTargetRef.current.x = x;
mouseTargetRef.current.y = y;
pointerPosRef.current.active = false; // Ensure loop doesn't override
} else { } 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; uniforms.iTime.value = t * 0.001;
// Update target based on pointer position and scroll offset // 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; const scale = (renderer as unknown as { dpr?: number }).dpr || 1;
let x, y;
if (rectRef.current) { const dx = window.scrollX - scrollPosRef.current.x;
const dx = window.scrollX - scrollPosRef.current.x; const dy = window.scrollY - scrollPosRef.current.y;
const dy = window.scrollY - scrollPosRef.current.y; const rectLeft = rectRef.current.left - dx;
const rectLeft = rectRef.current.left - dx; const rectTop = rectRef.current.top - dy;
const rectTop = rectRef.current.top - dy;
x = (pointerPosRef.current.x - rectLeft) * scale; const x = (pointerPosRef.current.x - rectLeft) * scale;
y = (rectRef.current.height - (pointerPosRef.current.y - rectTop)) * scale; const y = (rectRef.current.height - (pointerPosRef.current.y - rectTop)) * scale;
} else {
// Fallback if rectRef missing mouseTargetRef.current.x = x;
const rect = canvas.getBoundingClientRect(); mouseTargetRef.current.y = y;
x = (pointerPosRef.current.x - rect.left) * scale;
y = (rect.height - (pointerPosRef.current.y - rect.top)) * scale;
}
mouseTargetRef.current = [x, y];
} }
if (mouseDampening > 0) { if (mouseDampening > 0) {
@@ -373,11 +370,12 @@ void main() {
if (factor > 1) factor = 1; if (factor > 1) factor = 1;
const target = mouseTargetRef.current; const target = mouseTargetRef.current;
const cur = uniforms.iMouse.value; const cur = uniforms.iMouse.value;
cur[0] += (target[0] - cur[0]) * factor; cur[0] += (target.x - cur[0]) * factor;
cur[1] += (target[1] - cur[1]) * factor; cur[1] += (target.y - cur[1]) * factor;
} else { } else {
if (pointerPosRef.current || isMobileRef.current) { if (pointerPosRef.current.active || isMobileRef.current) {
uniforms.iMouse.value = mouseTargetRef.current; uniforms.iMouse.value[0] = mouseTargetRef.current.x;
uniforms.iMouse.value[1] = mouseTargetRef.current.y;
} }
lastTimeRef.current = t; lastTimeRef.current = t;
} }