نشتی حافظهٔ SPA رو با soak test بگیر
خلاصهٔ کاملتر
تیمهای بکاند سالهاست برای پیدا کردن نشتی حافظه سراغ soak test میرن: ساعتها ترافیک ساختگی به سرویس میزنن و حافظهٔ آخر کار رو با شروع مقایسه میکنن. نویسنده تو این مقاله میگه فرانتاند دیگه از این قاعده جدا نیست؛ چون SPA هیچوقت reload نمیشه، هیچی حافظهش رو ریست نمیکنه و یه نشتی کوچیک هم آرومآروم بزرگ میشه. به گفتهٔ نویسنده بعضی تیمها هر چند ساعت اپشون رو زورکی refresh میکنن فقط که همین حافظه خالی شه.
یه تحلیل استاتیک روی ۵۰۰ ریپازیتوری محبوب React و Vue و Angular که اوایل ۲۰۲۶ منتشر شده، نشون داده ۸۶٪ اونها یه listener یا timer یا subscription میسازن و هیچوقت حذفش نمیکنن؛ تایمرهای پاکنشده بهتنهایی نزدیک ۴۴٪ یافتهها بودن. تست end-to-end معمولی اینها رو نمیگیره، چون هر تست با یه browser context تازه شروع میشه و سریع تموم میشه، ولی نشتی وقت لازم داره تا اندازهش قابل اندازهگیری بشه.
ایده سادهست: یه فلوی رفتوبرگشتی مثل باز و بسته کردن یه drawer رو ۲۰۰ بار توی یک browser context تکرار میکنی و انتظار داری آخرش حافظه نزدیک نقطهٔ شروع باشه. اعداد از طریق Chrome DevTools Protocol خونده میشن (پس فعلاً فقط روی Chromium جواب میده) و قبلش دوبار جمعآوری زباله انجام میشه تا شمارشها پایدار بمونن:
await client.send('HeapProfiler.collectGarbage');
await client.send('HeapProfiler.collectGarbage');
const { metrics } = await client.send('Performance.getMetrics');
const { JSHeapUsedSize, Nodes, JSEventListeners } = Object.fromEntries(
metrics.map((m) => [m.name, m.value])
);چون JSHeapUsedSize بین اجراها بالا و پایین میره، assert اصلی روی شمارش نود و event listener بسته میشه. ۵ حلقهٔ اول هم بهعنوان warmup قبل از گرفتن baseline اجرا میشن تا بارگذاری اولیهٔ کد و دیتای drawer نتیجه رو خراب نکنه، و بهجای درصد یه آستانهٔ ثابت ۱۰۰ نودی گذاشته شده که بالای نوسان طبیعیه:
const { baseline, after } = await soak(page, () => openAndCloseDrawer(page));
expect(after.listeners).toBeLessThanOrEqual(baseline.listeners);
expect(after.nodes).toBeLessThan(baseline.nodes + 100);کلیکها همهٔ ماجرا نیستن: تایمرها روی ساعت واقعی مرورگر میخوابن، پس ۲۰۰ حلقه توی دو دقیقه یه polling سیثانیهای رو فقط چهار بار اجرا میکنه. راهحل نویسنده جعل ساعته؛ page.clock.install قبل از بالا اومدن اپ، بعد pauseAt و توی هر پاس runFor که روی هم یک ساعت تایمر میشه. fetch با ساعت جعلی جلو نمیره، پس شبکه رو هم باید با page.route جواب داد و حجم جواب mock باید نزدیک جواب واقعی باشه.
نکات کلیدی:
- soak test فرانتاند یعنی یه فلوی رفتوبرگشتی چند صد بار توی یک browser context
- assert روی شمارش نود و event listener، نه روی حجم heap که نوسان داره
- ۵ حلقه warmup قبل از baseline و دوبار garbage collection برای پایدار شدن اعداد
- جعل ساعت با page.clock و mock شبکه با page.route تا ساعتها استفادهٔ واقعی فشرده بشه
- برای وبسوکت page.routeWebSocket و برای پیدا کردن منبع نشتی، فیلتر Detached تو پنل Memory
- جای اجرا روی هر PR، تو جاب شبانه؛ چون اعداد بین اجراها نوسان دارن




