From eaac4ef0e1025a0bffc47c9cb6e8476ff21bef1b Mon Sep 17 00:00:00 2001 From: WhoamiI00 Date: Wed, 12 Aug 2026 18:48:00 +0530 Subject: [PATCH] fix(bento): dispose homepage animation listeners and timers on unmount The bento animation components register motion `hover()` and `inView()` handlers inside `$effect`/`onMount` but discard the disposer each one returns, and never cancel the in-flight animation. Nothing is torn down when the component is destroyed by a client-side navigation. Two consequences: - Every card leaks its IntersectionObserver. Leaving the homepage left 10 of 12 observers alive, holding a reference to the detached subtree. - `auth.svelte` starts a 1000ms `write()` interval on hover. Clicking the card mid-animation navigates away, but the interval keeps running and its `.then()` still fires `animate(button, ...)` after `bind:this` has reset `button` to null, throwing "You're trying to perform an animation on null". `sites.svelte` similarly leaves a 44s `animate()` running. Capture the disposers, return a teardown from the effect, and stop any in-flight animation. `auth.svelte` also guards the deferred button pulse, since a write that settles exactly as the component unmounts resolves after teardown has already run. Measured on the homepage, hovering the Auth card then clicking it: before after animation ticks after unmount 11 0 IntersectionObservers still live 10 3 The 7 disposed observers are exactly the 7 bento cards; the remaining 3 belong to other components on the page. --- .../bento/(animations)/auth.svelte | 27 +++++++++++++------ .../bento/(animations)/databases.svelte | 9 +++++-- .../bento/(animations)/functions.svelte | 11 ++++++-- .../bento/(animations)/messaging.svelte | 9 +++++-- .../bento/(animations)/realtime.svelte | 9 +++++-- .../bento/(animations)/sites.svelte | 16 +++++++++-- .../bento/(animations)/storage.svelte | 9 +++++-- 7 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte index 1b3127a30f3..8133e6cfe83 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte @@ -16,7 +16,15 @@ let currentAnimation: WriteAnimation | null = null; $effect(() => { - inView( + // The write animation can settle after the component has been destroyed + // (e.g. the user clicks the card mid-animation), by which point + // `bind:this` has already reset `button` to null. + const pulseButton = () => { + if (!button) return; + animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); + }; + + const stopInView = inView( container, () => { if (!isMobile()) return; @@ -28,9 +36,7 @@ 1000, password.length ); - currentAnimation.then(() => { - animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); - }); + currentAnimation.then(pulseButton); return () => { currentAnimation?.cancel(); currentAnimation = unwrite( @@ -43,14 +49,12 @@ { amount: 'all' } ); - hover(container, () => { + const stopHover = hover(container, () => { if (isMobile()) return; currentAnimation?.cancel(); currentAnimation = write('•••••••••••••', (v) => (password = v), 1000, password.length); - currentAnimation.then(() => { - animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); - }); + currentAnimation.then(pulseButton); return () => { currentAnimation?.cancel(); currentAnimation = unwrite( @@ -60,6 +64,13 @@ ); }; }); + + return () => { + stopInView(); + stopHover(); + currentAnimation?.cancel(); + currentAnimation = null; + }; }); diff --git a/src/routes/(marketing)/(components)/bento/(animations)/databases.svelte b/src/routes/(marketing)/(components)/bento/(animations)/databases.svelte index 526776a59fe..8ab79b4b243 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/databases.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/databases.svelte @@ -65,7 +65,7 @@ let shouldAnimate = $state(false); $effect(() => { - hover(container, () => { + const stopHover = hover(container, () => { if (isMobile()) return; animate( table, @@ -99,7 +99,7 @@ }; }); - inView( + const stopInView = inView( container, () => { if (!isMobile()) return; @@ -137,6 +137,11 @@ }, { amount: 'all' } ); + + return () => { + stopHover(); + stopInView(); + }; }); diff --git a/src/routes/(marketing)/(components)/bento/(animations)/functions.svelte b/src/routes/(marketing)/(components)/bento/(animations)/functions.svelte index f20f91630ab..13f61447bce 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/functions.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/functions.svelte @@ -25,7 +25,7 @@ $effect(() => { baseWidth = activeCommand.offsetWidth; - hover(container, () => { + const stopHover = hover(container, () => { if (isMobile()) return; widthAnim?.stop(); @@ -46,7 +46,7 @@ }; }); - inView( + const stopInView = inView( container, () => { if (!isMobile()) return; @@ -70,6 +70,13 @@ }, { amount: 'all' } ); + + return () => { + stopHover(); + stopInView(); + widthAnim?.stop(); + widthAnim = null; + }; }); diff --git a/src/routes/(marketing)/(components)/bento/(animations)/messaging.svelte b/src/routes/(marketing)/(components)/bento/(animations)/messaging.svelte index 5871c056ab7..975481b6195 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/messaging.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/messaging.svelte @@ -26,7 +26,7 @@ [notification, { opacity: 1, y: 0, filter: 'blur(0px)' }, { duration: 0.2, at: 0.15 }] ]; - inView( + const stopInView = inView( container, () => { if (!isMobile()) return; @@ -39,7 +39,7 @@ { amount: 'all' } ); - hover(container, () => { + const stopHover = hover(container, () => { if (isMobile()) return; animate(to); @@ -47,6 +47,11 @@ animate(from); }; }); + + return () => { + stopInView(); + stopHover(); + }; }); diff --git a/src/routes/(marketing)/(components)/bento/(animations)/realtime.svelte b/src/routes/(marketing)/(components)/bento/(animations)/realtime.svelte index 462c26a68ca..25fc97a2e86 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/realtime.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/realtime.svelte @@ -34,7 +34,7 @@ [topRightCursor, { scale: 1 }, { duration: 0.25, at: 0.35 }] ]; - inView( + const stopInView = inView( container, () => { if (!isMobile()) return; @@ -47,7 +47,7 @@ { amount: 'all' } ); - hover(container, () => { + const stopHover = hover(container, () => { if (isMobile()) return; animate(to); @@ -55,6 +55,11 @@ animate(from); }; }); + + return () => { + stopInView(); + stopHover(); + }; }); diff --git a/src/routes/(marketing)/(components)/bento/(animations)/sites.svelte b/src/routes/(marketing)/(components)/bento/(animations)/sites.svelte index 4edae6cff39..964f17e89ec 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/sites.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/sites.svelte @@ -46,9 +46,10 @@ ]; let shouldAnimate = $state(false); + let secondsAnimation: ReturnType | null = null; $effect(() => { - hover(container, () => { + const stopHover = hover(container, () => { if (isMobile()) return; shouldAnimate = true; @@ -57,6 +58,7 @@ onUpdate: (latest) => (seconds = +latest.toFixed()), duration: 44 }); + secondsAnimation = animation; currentAnimation?.cancel(); currentAnimation = write( @@ -82,7 +84,7 @@ }; }); - inView( + const stopInView = inView( container, () => { if (!isMobile()) return; @@ -92,6 +94,7 @@ onUpdate: (latest) => (seconds = +latest.toFixed()), duration: 44 }); + secondsAnimation = animation; currentAnimation?.cancel(); currentAnimation = write( @@ -118,6 +121,15 @@ }, { amount: 'all' } ); + + return () => { + stopHover(); + stopInView(); + secondsAnimation?.stop(); + secondsAnimation = null; + currentAnimation?.cancel(); + currentAnimation = null; + }; }); diff --git a/src/routes/(marketing)/(components)/bento/(animations)/storage.svelte b/src/routes/(marketing)/(components)/bento/(animations)/storage.svelte index 1129572444d..c09f9b276ee 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/storage.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/storage.svelte @@ -10,7 +10,7 @@ let image: HTMLElement; $effect(() => { - hover(container, () => { + const stopHover = hover(container, () => { if (isMobile()) return; animate(image, { borderRadius: '24px', filter: 'grayscale(25%)' }, { duration: 0.2 }); @@ -27,7 +27,7 @@ }; }); - inView( + const stopInView = inView( container, () => { if (!isMobile()) return; @@ -53,6 +53,11 @@ amount: 'all' } ); + + return () => { + stopHover(); + stopInView(); + }; });