نشت های حافظه مشکل ساز ناشی از جدا شدن پنجره ها را پیدا و رفع کنید.
نشت حافظه در جاوا اسکریپت چیست؟
نشت حافظه افزایش ناخواسته مقدار حافظه مورد استفاده یک برنامه در طول زمان است. در جاوا اسکریپت، نشت حافظه زمانی اتفاق می افتد که اشیا دیگر مورد نیاز نباشند، اما همچنان توسط توابع یا اشیاء دیگر ارجاع داده شوند. این ارجاعات از بازیابی اشیاء غیر ضروری توسط زباله جمع کن جلوگیری می کند.
وظیفه جمع آوری زباله شناسایی و بازیابی اشیایی است که دیگر از طریق برنامه قابل دسترسی نیستند. این کار حتی زمانی که اشیا به خود ارجاع می دهند، یا به صورت دوره ای به یکدیگر ارجاع می دهند، کار می کند – زمانی که هیچ مرجعی وجود نداشته باشد که از طریق آن یک برنامه بتواند به گروهی از اشیاء دسترسی داشته باشد، می تواند زباله جمع آوری شود.
let A = {};
console.log(A); // local variable reference
let B = {A}; // B.A is a second reference to A
A = null; // unset local variable reference
console.log(B.A); // A can still be referenced by B
B.A = null; // unset B's reference to A
// No references to A are left. It can be garbage collected.
یک کلاس خاص از نشت حافظه زمانی رخ می دهد که یک برنامه به اشیایی که چرخه حیات خاص خود را دارند، مانند عناصر DOM یا پنجره های بازشو ارجاع دهد. ممکن است این نوع اشیاء بدون اطلاع برنامه بلااستفاده شوند، به این معنی که کد برنامه ممکن است تنها ارجاعات باقیمانده به یک شی را داشته باشد که در غیر این صورت می تواند زباله جمع آوری شود.
پنجره جدا چیست؟
در مثال زیر، یک برنامه نمایش اسلایدی شامل دکمه هایی برای باز و بسته کردن پنجره یادداشت های ارائه دهنده است. تصور کنید کاربری روی Show Notes کلیک کند، سپس بهجای کلیک کردن روی دکمه Hide Notes ، پنجره بازشو را مستقیماً میبندد – متغیر notesWindow
همچنان ارجاعی به پنجره بازشوی قابل دسترسی دارد، حتی اگر پنجره بازشو دیگر استفاده نمیشود.
<button id="show">Show Notes</button>
<button id="hide">Hide Notes</button>
<script type="module">
let notesWindow;
document.getElementById('show').onclick = () => {
notesWindow = window.open('/presenter-notes.html');
};
document.getElementById('hide').onclick = () => {
if (notesWindow) notesWindow.close();
};
</script>
این نمونه ای از پنجره جدا شده است. پنجره بازشو بسته شد، اما کد ما یک مرجع به آن دارد که از مرورگر نمیتواند آن را از بین ببرد و آن حافظه را بازیابی کند.
وقتی صفحه ای window.open()
را برای ایجاد یک پنجره یا برگه مرورگر جدید فراخوانی می کند، یک شی Window
برگردانده می شود که نمایانگر پنجره یا برگه است. حتی پس از بسته شدن چنین پنجره ای یا دور شدن کاربر از آن، شی Window
که از window.open()
بازگردانده می شود همچنان می تواند برای دسترسی به اطلاعات مربوط به آن استفاده شود. این یکی از انواع پنجرههای جدا شده است: چون کد جاوا اسکریپت همچنان میتواند به طور بالقوه به ویژگیهای شی Window
بسته دسترسی داشته باشد، باید در حافظه نگهداری شود. اگر پنجره شامل بسیاری از اشیاء جاوا اسکریپت یا iframe باشد، تا زمانی که هیچ مرجع جاوا اسکریپتی به خصوصیات پنجره وجود نداشته باشد، نمی توان آن حافظه را بازیابی کرد.
هنگام استفاده از عناصر <iframe>
نیز همین مشکل ممکن است رخ دهد. Iframe ها مانند پنجره های تو در تو که حاوی اسناد هستند رفتار می کنند و ویژگی contentWindow
آنها دسترسی به شی Window
موجود را فراهم می کند، دقیقاً شبیه مقدار بازگردانده شده توسط window.open()
. کد جاوا اسکریپت میتواند ارجاع به contentWindow
یا contentDocument
یک iframe را حفظ کند، حتی اگر iframe از DOM حذف شود یا URL آن تغییر کند، که از جمعآوری زبالههای سند جلوگیری میکند زیرا هنوز میتوان به ویژگیهای آن دسترسی داشت.
در مواردی که ارجاع به document
در یک پنجره یا iframe از جاوا اسکریپت حفظ میشود، آن سند در حافظه نگه داشته میشود، حتی اگر پنجره یا iframe حاوی به یک URL جدید هدایت شود. وقتی جاوا اسکریپتی که آن مرجع را نگه میدارد، تشخیص نمیدهد که پنجره/قاب به یک URL جدید هدایت شده است، میتواند دردسرساز شود، زیرا نمیداند چه زمانی آخرین مرجعی است که یک سند را در حافظه نگه میدارد.
چگونه پنجره های جدا شده باعث نشت حافظه می شوند
هنگام کار با ویندوز و iframe در همان دامنه صفحه اصلی، گوش دادن به رویدادها یا دسترسی به ویژگی ها در سراسر مرزهای سند معمول است. به عنوان مثال، اجازه دهید از ابتدای این راهنما، تغییری در نمونه نمایشگر ارائه ارائه کنیم. بیننده پنجره دومی را برای نمایش یادداشت های گوینده باز می کند. پنجره یادداشتهای بلندگو به رویدادهای click
گوش میدهد تا به اسلاید بعدی منتقل شود. اگر کاربر این پنجره یادداشت ها را ببندد، جاوا اسکریپت در حال اجرا در پنجره والد اصلی همچنان به سند یادداشت های بلندگو دسترسی کامل دارد:
<button id="notes">Show Presenter Notes</button>
<script type="module">
let notesWindow;
function showNotes() {
notesWindow = window.open('/presenter-notes.html');
notesWindow.document.addEventListener('click', nextSlide);
}
document.getElementById('notes').onclick = showNotes;
let slide = 1;
function nextSlide() {
slide += 1;
notesWindow.document.title = `Slide ${slide}`;
}
document.body.onclick = nextSlide;
</script>
تصور کنید پنجره مرورگر ایجاد شده توسط showNotes()
در بالا را می بندیم. هیچ کنترل کننده رویدادی برای تشخیص بسته بودن پنجره گوش نمی دهد، بنابراین هیچ چیز به کد ما اطلاع نمی دهد که باید هر گونه ارجاع به سند را پاک کند. تابع nextSlide()
همچنان "live" است زیرا به عنوان یک کنترل کننده کلیک در صفحه اصلی ما محدود شده است، و این واقعیت که nextSlide
حاوی ارجاع به notesWindow
است به این معنی است که پنجره هنوز ارجاع دارد و نمی توان زباله را جمع آوری کرد.
تعدادی سناریو دیگر وجود دارد که در آن منابع به طور تصادفی حفظ می شوند که از واجد شرایط بودن پنجره های جدا شده برای جمع آوری زباله جلوگیری می کند:
کنترلکنندههای رویداد را میتوان در سند اولیه iframe ثبت کرد، قبل از اینکه فریم به URL مورد نظر خود پیمایش کند، که منجر به ارجاعات تصادفی به سند میشود و iframe پس از پاکسازی سایر مراجع باقی میماند.
یک سند پرحافظه بارگذاری شده در یک پنجره یا iframe می تواند به طور تصادفی مدت ها پس از پیمایش به یک URL جدید در حافظه نگهداری شود. این اغلب به دلیل حفظ ارجاعات صفحه اصلی به سند به منظور حذف شنونده ایجاد می شود.
هنگام ارسال یک شی جاوا اسکریپت به پنجره یا iframe دیگر، زنجیره نمونه اولیه شی شامل ارجاعاتی به محیطی است که در آن ایجاد شده است، از جمله پنجره ای که آن را ایجاد کرده است. این بدان معناست که به همان اندازه مهم است که از نگه داشتن ارجاع به اشیاء از پنجره های دیگر خودداری کنید.
index.html:
<script> let currentFiles; function load(files) { // this retains the popup: currentFiles = files; } window.open('upload.html'); </script>
upload.html:
<input type="file" id="file" /> <script> file.onchange = () => { parent.load(file.files); }; </script>
تشخیص نشت حافظه ناشی از جدا شدن پنجره ها
ردیابی نشت حافظه می تواند مشکل باشد. اغلب ساختن بازتولیدهای مجزا از این مسائل دشوار است، به ویژه هنگامی که اسناد یا پنجره های متعددی درگیر هستند. برای پیچیدهتر کردن همه چیز، بازرسی منابع لو رفته بالقوه میتواند منجر به ایجاد مراجع اضافی شود که از جمعآوری زبالههای اشیاء بازرسیشده جلوگیری میکند. برای این منظور، شروع با ابزارهایی که به طور خاص از ارائه این امکان اجتناب می کنند، مفید است.
یک مکان عالی برای شروع اشکال زدایی مشکلات حافظه ، گرفتن عکس فوری است. این یک نمای نقطه در زمان به حافظه ای که در حال حاضر توسط یک برنامه استفاده می شود ارائه می دهد - همه اشیایی که ایجاد شده اند اما هنوز زباله جمع آوری نشده اند. عکسهای فوری هیپ حاوی اطلاعات مفیدی درباره اشیا، از جمله اندازه آنها و فهرستی از متغیرها و بستههایی است که به آنها اشاره میکنند.
برای ضبط عکس فوری پشته، به تب Memory در Chrome DevTools بروید و Heap Snapshot را در لیست انواع پروفایل موجود انتخاب کنید. پس از پایان ضبط، نمای خلاصه ، اشیاء موجود در حافظه را نشان می دهد که بر اساس سازنده گروه بندی شده اند.
تجزیه و تحلیل heap dumps می تواند یک کار دلهره آور باشد و یافتن اطلاعات مناسب به عنوان بخشی از اشکال زدایی می تواند بسیار دشوار باشد. برای کمک به این امر، مهندسان Chromium yossik@ و peledni@ ابزار مستقل Heap Cleaner را توسعه دادند که می تواند به برجسته کردن یک گره خاص مانند یک پنجره جدا شده کمک کند. اجرای Heap Cleaner بر روی یک ردیابی، سایر اطلاعات غیر ضروری را از نمودار حفظ حذف می کند، که باعث می شود ردیابی تمیزتر و خواندن آن بسیار آسان تر شود.
اندازه گیری حافظه به صورت برنامه ای
عکسهای فوری پشتهای سطح بالایی از جزئیات را ارائه میکنند و برای تشخیص محل نشتی عالی هستند، اما گرفتن عکس فوری پشتهای یک فرآیند دستی است. راه دیگر برای بررسی نشت حافظه این است که اندازه پشته جاوا اسکریپت مورد استفاده فعلی را از API performance.memory
بدست آورید:
API performance.memory
فقط اطلاعات مربوط به اندازه پشته جاوا اسکریپت را ارائه می دهد، به این معنی که شامل حافظه مورد استفاده توسط سند و منابع پنجره بازشو نمی شود. برای دریافت تصویر کامل، باید از API جدید performance.measureUserAgentSpecificMemory()
استفاده کنیم که در حال حاضر در کروم آزمایش می شود.
راهکارهایی برای جلوگیری از نشت پنجره جدا شده
دو مورد متداول که در آن پنجرههای جدا شده باعث نشت حافظه میشوند، زمانی است که سند والد ارجاعات به یک پنجره بازشو بسته یا iframe حذف شده را حفظ میکند، و زمانی که پیمایش غیرمنتظره یک پنجره یا iframe باعث میشود که کنترلکننده رویداد هرگز ثبت نشود.
مثال: بستن پنجره بازشو
در مثال زیر از دو دکمه برای باز و بسته کردن یک پنجره بازشو استفاده شده است. برای اینکه دکمه Close Popup کار کند، یک مرجع به پنجره باز شده در یک متغیر ذخیره می شود:
<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
};
</script>
در نگاه اول، به نظر می رسد که کد بالا از مشکلات رایج جلوگیری می کند: هیچ ارجاعی به سند پاپ آپ حفظ نمی شود و هیچ کنترل کننده رویداد در پنجره بازشو ثبت نمی شود. با این حال، پس از کلیک بر روی دکمه Open Popup ، متغیر popup
اکنون به پنجره باز شده ارجاع می دهد، و آن متغیر از محدوده کنترل کننده کلیک دکمه Close Popup قابل دسترسی است. مگر اینکه popup
بازشو مجدداً اختصاص داده شود یا کنترل کننده کلیک حذف شود، ارجاع محصور آن کنترل کننده به popup
به این معنی است که نمی توان آن را زباله جمع کرد.
راه حل: ارجاعات را تنظیم نکنید
متغیرهایی که به پنجره دیگری یا سند آن اشاره می کنند باعث می شوند که آن در حافظه باقی بماند. از آنجایی که اشیاء در جاوا اسکریپت همیشه مرجع هستند، با اختصاص یک مقدار جدید به متغیرها، ارجاع آنها به شی اصلی حذف می شود. برای «تنظیم کردن» ارجاعات به یک شی، میتوانیم آن متغیرها را دوباره به مقدار null
نسبت دهیم.
با اعمال این مورد در مثال پاپ آپ قبلی، می توانیم کنترل کننده دکمه بستن را تغییر دهیم تا مرجع خود را به پنجره بازشو "تنظیم" کند:
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
popup = null;
};
این کمک میکند، اما یک مشکل بیشتر مخصوص ویندوزهای ایجاد شده با استفاده از open()
را نشان میدهد: اگر کاربر به جای کلیک کردن روی دکمه بستن سفارشی ما، پنجره را ببندد چه؟ علاوه بر این، اگر کاربر در پنجرهای که باز کردیم شروع به مرور وبسایتهای دیگر کند، چه؟ در حالی که در ابتدا کافی به نظر می رسید که هنگام کلیک کردن روی دکمه بستن ما، مرجع popup
تنظیم نشود، زمانی که کاربران از آن دکمه خاص برای بستن پنجره استفاده نمی کنند، هنوز نشت حافظه وجود دارد. حل این امر مستلزم شناسایی این موارد است تا در صورت وقوع، مراجع طولانی را از بین ببریم.
راه حل: نظارت و دور ریختن
در بسیاری از موقعیت ها، جاوا اسکریپت که مسئول باز کردن پنجره ها یا ایجاد فریم است، کنترل انحصاری بر چرخه عمر آنها ندارد. پنجرههای بازشو میتوانند توسط کاربر بسته شوند، یا پیمایش به یک سند جدید میتواند باعث شود سندی که قبلاً در یک پنجره یا قاب قرار داشت، جدا شود. در هر دو مورد، مرورگر یک رویداد pagehide
را اجرا می کند تا نشان دهد که سند در حال بارگیری است.
رویداد pagehide
را می توان برای شناسایی پنجره های بسته و ناوبری به دور از سند فعلی استفاده کرد. با این حال، یک اخطار مهم وجود دارد: همه پنجرهها و iframeهای تازه ایجاد شده حاوی یک سند خالی هستند، سپس در صورت ارائه به صورت ناهمزمان به URL داده شده بروید. در نتیجه، یک رویداد pagehide
اولیه اندکی پس از ایجاد پنجره یا فریم، درست قبل از بارگیری سند هدف، اجرا میشود. از آنجایی که کد پاکسازی مرجع ما باید هنگام بارگیری سند مورد نظر اجرا شود، باید این اولین رویداد pagehide
را نادیده بگیریم. تعدادی تکنیک برای انجام این کار وجود دارد که ساده ترین آنها نادیده گرفتن رویدادهای pagehide است که از URL سند اولیه about:blank
نشات می گیرد. در مثال پاپ آپ ما چگونه به نظر می رسد:
let popup;
open.onclick = () => {
popup = window.open('/login.html');
// listen for the popup being closed/exited:
popup.addEventListener('pagehide', () => {
// ignore initial event fired on "about:blank":
if (!popup.location.host) return;
// remove our reference to the popup window:
popup = null;
});
};
توجه به این نکته مهم است که این تکنیک فقط برای پنجرهها و فریمهایی کار میکند که منشأ مؤثری مشابه صفحه اصلی دارند که کد ما در آن اجرا میشود. هنگام بارگیری محتوا از مبدا متفاوت، هم location.host
و هم رویداد pagehide
به دلایل امنیتی در دسترس نیستند. در حالی که به طور کلی بهتر است از ارجاع به منابع دیگر اجتناب شود، در موارد نادری که این مورد نیاز است، امکان نظارت بر ویژگی های window.closed
یا frame.isConnected
وجود دارد. هنگامی که این ویژگیها برای نشان دادن یک پنجره بسته یا iframe حذف شده تغییر میکنند، بهتر است هر گونه ارجاع به آن را حذف کنید.
let popup = window.open('https://example.com');
let timer = setInterval(() => {
if (popup.closed) {
popup = null;
clearInterval(timer);
}
}, 1000);
راه حل: از WeakRef استفاده کنید
جاوا اسکریپت اخیراً از روش جدیدی برای ارجاع به اشیاء پشتیبانی کرده است که به جمع آوری زباله اجازه می دهد تا WeakRef
نامیده شود. یک WeakRef
ایجاد شده برای یک شی، یک مرجع مستقیم نیست، بلکه یک شی جداگانه است که یک متد .deref()
ویژه را ارائه می دهد که تا زمانی که شیء جمع آوری نشده باشد، یک مرجع را به آن شی برمی گرداند. با WeakRef
، دسترسی به مقدار فعلی یک پنجره یا سند امکان پذیر است و در عین حال امکان جمع آوری زباله وجود دارد. به جای حفظ ارجاع به پنجره ای که باید به صورت دستی در پاسخ به رویدادهایی مانند pagehide
یا ویژگی هایی مانند window.closed
تنظیم شود، دسترسی به پنجره در صورت نیاز به دست می آید. هنگامی که پنجره بسته است، میتوان زبالهها را جمعآوری کرد و باعث میشود که متد .deref()
undefined
شروع به بازگشت کند.
<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
let popup;
open.onclick = () => {
popup = new WeakRef(window.open('/login.html'));
};
close.onclick = () => {
const win = popup.deref();
if (win) win.close();
};
</script>
یکی از جزئیات جالبی که هنگام استفاده از WeakRef
برای دسترسی به ویندوز یا اسناد باید در نظر گرفت این است که مرجع معمولاً برای مدت کوتاهی پس از بسته شدن پنجره یا حذف iframe در دسترس باقی می ماند. این به این دلیل است که WeakRef
به بازگرداندن یک مقدار ادامه میدهد تا زمانی که شیء مرتبط با آن جمعآوری شود، که به صورت ناهمزمان در جاوا اسکریپت و عموماً در زمان بیکاری اتفاق میافتد. خوشبختانه، هنگام بررسی پنجرههای جداشده در پانل حافظه Chrome DevTools، گرفتن یک عکس فوری پشتهای در واقع باعث جمعآوری زباله میشود و پنجرهای که دارای مرجع ضعیف است را از بین میبرد. همچنین میتوان بررسی کرد که یک شی ارجاعشده از طریق WeakRef
از جاوا اسکریپت حذف شده است، یا با تشخیص زمانی که deref()
undefined
برمیگرداند یا با استفاده از FinalizationRegistry
API جدید:
let popup = new WeakRef(window.open('/login.html'));
// Polling deref():
let timer = setInterval(() => {
if (popup.deref() === undefined) {
console.log('popup was garbage-collected');
clearInterval(timer);
}
}, 20);
// FinalizationRegistry API:
let finalizers = new FinalizationRegistry(() => {
console.log('popup was garbage-collected');
});
finalizers.register(popup.deref());
راه حل: از طریق postMessage
ارتباط برقرار کنید
تشخیص زمانی که پنجره ها بسته هستند یا پیمایش یک سند را تخلیه می کند، راهی به ما می دهد تا کنترل کننده ها و ارجاعات تنظیم نشده را حذف کنیم تا پنجره های جدا شده را بتوان زباله جمع آوری کرد. با این حال، این تغییرات اصلاحات خاصی برای چیزی است که گاهی اوقات می تواند یک نگرانی اساسی تر باشد: جفت مستقیم بین صفحات.
یک رویکرد جایگزین جامعتر در دسترس است که از ارجاعات قدیمی بین پنجرهها و اسناد جلوگیری میکند: ایجاد جداسازی با محدود کردن ارتباطات بین اسناد به postMessage()
. با فکر کردن به مثال یادداشت های ارائه دهنده اصلی، توابعی مانند nextSlide()
پنجره یادداشت ها را مستقیماً با ارجاع به آن و دستکاری محتوای آن به روز می کنند. در عوض، صفحه اصلی می تواند اطلاعات لازم را به صورت ناهمزمان و غیرمستقیم از طریق postMessage()
به پنجره یادداشت ها منتقل کند.
let updateNotes;
function showNotes() {
// keep the popup reference in a closure to prevent outside references:
let win = window.open('/presenter-view.html');
win.addEventListener('pagehide', () => {
if (!win || !win.location.host) return; // ignore initial "about:blank"
win = null;
});
// other functions must interact with the popup through this API:
updateNotes = (data) => {
if (!win) return;
win.postMessage(data, location.origin);
};
// listen for messages from the notes window:
addEventListener('message', (event) => {
if (event.source !== win) return;
if (event.data[0] === 'nextSlide') nextSlide();
});
}
let slide = 1;
function nextSlide() {
slide += 1;
// if the popup is open, tell it to update without referencing it:
if (updateNotes) {
updateNotes(['setSlide', slide]);
}
}
document.body.onclick = nextSlide;
در حالی که این امر همچنان مستلزم ارجاع پنجره ها به یکدیگر است، هیچ کدام از یک پنجره دیگر به سند فعلی ارجاع نمی دهند. رویکرد ارسال پیام همچنین طرح هایی را تشویق می کند که در آن مراجع پنجره ها در یک مکان واحد نگهداری می شوند، به این معنی که تنها یک مرجع تنها باید هنگام بسته شدن یا دور شدن پنجره ها تنظیم شود. در مثال بالا، فقط showNotes()
یک ارجاع به پنجره یادداشت ها را حفظ می کند و از رویداد pagehide
برای اطمینان از پاک شدن مرجع استفاده می کند.
راه حل: از ارجاع با استفاده از noopener
اجتناب کنید
در مواردی که یک پنجره بازشو باز می شود که صفحه شما نیازی به برقراری ارتباط یا کنترل آن ندارد، ممکن است بتوانید از به دست آوردن ارجاع به پنجره جلوگیری کنید. این به ویژه هنگام ایجاد ویندوز یا iframe که محتوا را از سایت دیگری بارگیری می کند مفید است. برای این موارد، window.open()
گزینه "noopener"
را می پذیرد که درست مانند ویژگی rel="noopener"
برای پیوندهای HTML کار می کند:
window.open('https://example.com/share', null, 'noopener');
گزینه "noopener"
باعث می شود که window.open()
null
را برگرداند و ذخیره تصادفی یک مرجع به پنجره بازشو غیرممکن می شود. همچنین از دریافت ارجاع پنجره بازشو به پنجره والد خود جلوگیری می کند، زیرا ویژگی window.opener
null
خواهد بود.
بازخورد
امیدواریم برخی از پیشنهادات این مقاله به یافتن و رفع نشت حافظه کمک کند. اگر تکنیک دیگری برای اشکال زدایی ویندوزهای جدا شده دارید یا این مقاله به کشف نشت در برنامه شما کمک کرده است، مایلم بدانم! می توانید من را در توییتر @_developit پیدا کنید.