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