Skip to content

Commit 80eed7a

Browse files
committed
perf(celebration): drop per-particle text-shadow + will-change, gate spawn on visibility
1 parent 6c8e4df commit 80eed7a

1 file changed

Lines changed: 22 additions & 7 deletions

File tree

components/CelebrationEffect.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,10 @@ const particleBase = css`
257257
/* backwards applies opacity:0 during animation-delay; forwards pins the
258258
100% state so particles don't snap back to origin before unmount. */
259259
animation: ${burstAnim} var(--dur) ease-out var(--delay) both;
260-
will-change: translate, scale, opacity;
261260
`;
262261

263262
const ParticleCharBase = styled.span.attrs<ParticleAttrs>(particleAttrs)`
264263
${particleBase}
265-
text-shadow: 0 0 10px var(--particle-color);
266264
`;
267265

268266
const ParticleChar = React.memo(ParticleCharBase);
@@ -288,7 +286,7 @@ function generateParticles(x: number, y: number, color: string, delay: number):
288286
color,
289287
size: rand(cfg.fontSize[0], cfg.fontSize[1]),
290288
duration: rand(1.8, 3.3),
291-
// Stagger layer promotion across 2–3 frames instead of spiking on one.
289+
// Jitter ignition across a few frames so the burst doesn't all light up on the same tick.
292290
delay: delay + rand(0, 0.04),
293291
x,
294292
y,
@@ -344,25 +342,42 @@ export default function CelebrationEffect() {
344342
});
345343
}
346344

347-
spawnFirework();
348-
let timer: ReturnType<typeof setTimeout>;
345+
let timer: ReturnType<typeof setTimeout> | null = null;
349346
let stopped = false;
347+
let visible = true;
350348

351349
function scheduleNext() {
352350
timer = setTimeout(
353351
() => {
354-
if (stopped) return;
352+
timer = null;
353+
if (stopped || !visible) return;
355354
spawnFirework();
356355
scheduleNext();
357356
},
358357
rand(2000, 4000)
359358
);
360359
}
360+
361+
spawnFirework();
361362
scheduleNext();
362363

364+
// Long pages scroll the overlay off-screen. Without this, the rAF
365+
// animations keep painting invisible particles at full GPU cost.
366+
const io = new IntersectionObserver(([entry]) => {
367+
visible = entry.isIntersecting;
368+
if (!visible && timer != null) {
369+
clearTimeout(timer);
370+
timer = null;
371+
} else if (visible && timer == null && !stopped) {
372+
scheduleNext();
373+
}
374+
});
375+
if (overlayRef.current) io.observe(overlayRef.current);
376+
363377
return () => {
364378
stopped = true;
365-
clearTimeout(timer);
379+
if (timer != null) clearTimeout(timer);
380+
io.disconnect();
366381
};
367382
}, []);
368383

0 commit comments

Comments
 (0)