AstroのView Transitionsを有効化すると、リンククリックでページ遷移がSPA風に滑らかになります。便利。けれどページ遷移のたびに astro:page-load が発火することを忘れると、セットアップ系の処理が再実行され、イベントリスナーが累積していきます。
メモリリークだけでなく、Escapeキーで複数のモーダルが同時に閉じる、スクロールハンドラが2重に発火する、といった重複動作の原因になります。
症状: 遷移ごとにリスナーが累積する
典型的なAstroコンポーネントのインラインスクリプトです。
<header id="site-header">...</header>
<script>
function setupHeader() {
const header = document.getElementById('site-header');
if (!header) return;
const onScroll = () => {
header.dataset.scrolled = window.scrollY > 8 ? 'true' : 'false';
};
window.addEventListener('scroll', onScroll, { passive: true });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { /* drawer close */ }
});
}
setupHeader();
document.addEventListener('astro:page-load', setupHeader);
</script>
(Astro の <script> 内では TypeScript 構文が使えるため、as キャストや型注釈を書ける前提でサンプルを書いています。素の HTML に転用する際は型注釈を外してください。)
これで /about/、/services/、/notes/ とSPA風遷移すると、scroll リスナーと keydown リスナーが毎回1個ずつ追加されます。3ページ遷移後にはスクロールハンドラが4重に発火する状態です。
原因: 再実行のたびに解除されない
astro:page-load はView Transitionsのswapが完了したタイミングで発火します。セットアップを書き直す機会としては正しい。問題は、前回のリスナーを解除する処理がないこと。登録だけが積もります。
普通のSPAフレームワーク(React、Vue等)ならコンポーネント unmount のライフサイクルでリスナーを外しますが、Astroのインラインスクリプトはコンポーネントライフサイクル概念がなく、自前でクリーンアップを組む必要があります。
解決: AbortController + signal
最近のJavaScriptの AbortController を使えば、複数のリスナーを1つのsignalでまとめて外せます。
<script>
let headerAbort: AbortController | null = null;
function setupHeader() {
// 前回セットアップのリスナーを全部abort
headerAbort?.abort();
headerAbort = new AbortController();
const { signal } = headerAbort;
const header = document.getElementById('site-header');
if (!header) return;
const onScroll = () => {
header.dataset.scrolled = window.scrollY > 8 ? 'true' : 'false';
};
window.addEventListener('scroll', onScroll, { passive: true, signal });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { /* ... */ }
}, { signal });
// 他にも click や resize 等あれば全部 { signal } を渡す
}
setupHeader();
document.addEventListener('astro:page-load', setupHeader);
</script>
addEventListener の3引数目に signal を渡すと、そのsignalが abort() された瞬間にリスナーが全部外れます。セットアップを再実行する前に前回分をまとめて解除できます。
astro:before-swapで外す
astro:page-load はswap後に発火しますが、もう一つ astro:before-swap がswap直前に発火します。これも使うと、さらに整理されます。
document.addEventListener('astro:before-swap', () => {
headerAbort?.abort();
headerAbort = null;
});
ページ遷移開始時に前ページのリスナーを全部外し、新ページのセットアップで新規登録する流れになります。
二重初期化のガード
closureの外側で headerAbort を保持しておくと、同ページで setupHeader() が複数回呼ばれた場合(開発時のホットリロード、外部から手動再init)にも累積しません。headerAbort?.abort() を setupHeader() の冒頭に置けば、何度呼んでも前回分が必ず外れます。
Astro View Transitions は SPA 風遷移の体感を簡単に手に入れられる代わりに、ページライフサイクルのクリーンアップを意識する必要があります。signal を渡しておけば、abort 時に複数のリスナーをまとめて解除できます。