⚡ Bolt: Optimize WebGL loop to reduce GC and layout thrashing #54
@@ -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.
|
||||
|
||||
@@ -63,9 +63,10 @@ const GradientBlinds: React.FC<GradientBlindsProps> = ({
|
||||
const meshRef = useRef<Mesh | null>(null);
|
||||
const geometryRef = useRef<Geometry | 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
|
||||
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 lastTimeRef = useRef<number>(0);
|
||||
const firstResizeRef = useRef<boolean>(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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
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.currentwith the values ofxandy, which are initialized to 0 on line 322. IfrectRef.currentis falsy, the coordinates will be set to (0, 0).This differs from the desktop path (line 349), which only updates
mouseTargetRef.currentwhen bothpointerPosRef.current.activeANDrectRef.currentare truthy. For consistency and to truly "do nothing" as the comment suggests, the mobile path should also guard the writes tomouseTargetRef.currentwith a check forrectRef.current.