Bolt: Optimize WebGL loop to reduce GC and layout thrashing #54

Closed
ragusa-it wants to merge 1 commits from bolt-optimize-webgl-loop-6571928292931476466 into main
2 changed files with 33 additions and 31 deletions
Showing only changes of commit 1fe7db0e16 - Show all commits

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;
copilot-pull-request-reviewer[bot] commented 2026-02-04 01:41:57 +00:00 (Migrated from github.com)
Review

The comment on line 332 states "Fallback: do nothing if rect is not ready yet to avoid sync layout thrashing", but the code actually does write to mouseTargetRef.current with the values of x and y, which are initialized to 0 on line 322. If rectRef.current is falsy, the coordinates will be set to (0, 0).

This differs from the desktop path (line 349), which only updates mouseTargetRef.current when both pointerPosRef.current.active AND rectRef.current are truthy. For consistency and to truly "do nothing" as the comment suggests, the mobile path should also guard the writes to mouseTargetRef.current with a check for rectRef.current.

The comment on line 332 states "Fallback: do nothing if rect is not ready yet to avoid sync layout thrashing", but the code actually does write to `mouseTargetRef.current` with the values of `x` and `y`, which are initialized to 0 on line 322. If `rectRef.current` is falsy, the coordinates will be set to (0, 0). This differs from the desktop path (line 349), which only updates `mouseTargetRef.current` when both `pointerPosRef.current.active` AND `rectRef.current` are truthy. For consistency and to truly "do nothing" as the comment suggests, the mobile path should also guard the writes to `mouseTargetRef.current` with a check for `rectRef.current`.
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;
} }