fix: stop beams background from causing UI flickering in browser

Remove opacity from beamDash keyframes — opacity on SVG paths triggers
full-page repaints. Animate only stroke-dashoffset (compositor-friendly).
Reduce animated paths from 50 to 17 (every 3rd). Isolate container with
contain:strict + will-change:transform for own compositor layer.
This commit is contained in:
Fringg
2026-02-25 13:27:14 +03:00
parent d019953693
commit 7e89ccea5c

View File

@@ -67,6 +67,11 @@ function seededRandom(seed: number) {
return x - Math.floor(x); return x - Math.floor(x);
} }
// Animate only every 3rd path (17 beams) — enough visual density, 3x fewer repaints
const animatedPaths = paths
.map((path, i) => ({ path, paramIndex: i }))
.filter((_, i) => i % 3 === 0);
// Pre-compute beam animation params at module level (stable across renders) // Pre-compute beam animation params at module level (stable across renders)
const beamParams = paths.map((_, i) => ({ const beamParams = paths.map((_, i) => ({
duration: 10 + seededRandom(i) * 10, // 10-20s duration: 10 + seededRandom(i) * 10, // 10-20s
@@ -81,12 +86,13 @@ function ensureStyles() {
const style = document.createElement('style'); const style = document.createElement('style');
style.id = STYLE_ID; style.id = STYLE_ID;
// Only animate stroke-dashoffset — no opacity changes = no repaints
// The beam appears when dashoffset brings the segment into view
// and disappears when it exits — pure compositor work
style.textContent = ` style.textContent = `
@keyframes beamDash { @keyframes beamDash {
0% { stroke-dashoffset: 1; opacity: 0; } 0% { stroke-dashoffset: 1; }
2% { opacity: 1; } 100% { stroke-dashoffset: -1; }
80% { opacity: 1; }
100% { stroke-dashoffset: -1; opacity: 0; }
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
@@ -99,7 +105,10 @@ export default React.memo(function BackgroundBeams({ settings: _settings }: Prop
}, []); }, []);
return ( return (
<div className="absolute inset-0 overflow-hidden"> <div
className="absolute inset-0 overflow-hidden"
style={{ contain: 'strict', willChange: 'transform' }}
>
<svg <svg
className="pointer-events-none absolute inset-0 h-full w-full" className="pointer-events-none absolute inset-0 h-full w-full"
width="100%" width="100%"
@@ -116,19 +125,19 @@ export default React.memo(function BackgroundBeams({ settings: _settings }: Prop
strokeWidth="0.5" strokeWidth="0.5"
/> />
{/* Animated beams — CSS stroke-dashoffset animation, GPU composited */} {/* Animated beams — only every 3rd path (17 beams), pure stroke-dashoffset */}
{paths.map((path, i) => ( {animatedPaths.map(({ path, paramIndex }) => (
<path <path
key={i} key={paramIndex}
d={path} d={path}
stroke="url(#beamGradient)" stroke="url(#beamGradient)"
strokeWidth="0.5" strokeWidth="0.5"
strokeOpacity="0.5" strokeOpacity="0.4"
pathLength={1} pathLength={1}
strokeDasharray="0.15 0.85" strokeDasharray="0.12 0.88"
strokeDashoffset="1" strokeDashoffset="1"
style={{ style={{
animation: `beamDash ${beamParams[i].duration}s ease-in-out ${beamParams[i].delay}s infinite`, animation: `beamDash ${beamParams[paramIndex].duration}s ease-in-out ${beamParams[paramIndex].delay}s infinite`,
}} }}
/> />
))} ))}