
حتماً براتون پیش اومده که تبهای زیادی توی مرورگر باز داشته باشید. دارید کارهاتون انجام میدید و چند ساعتی میگذره. کم کم احساس میکنید سیستم کُند شده.
کنجکاو میشید، Task Manager رو باز میکنید یا نگاهی به مصرف RAM میندازید متوجه میشید که مرورگر یه مقدار زیادی از حافظه رو اشفال کرده. یا یک وباپلیکیشن که مدتهاست باهاش کاری ندارید، هنوز داره کلی از حافظهی سیستم رو اشغال میکنه.
پشتِ اون رابطهای کاربری جذاب و تجربهی روانی که میبینیم، اتفاقهایی در جریانه که همیشه هم به نفع ما نیستن. تب های مرورگری که بازن و خیلی از این وباپها، حجم زیادی از دادهها رو نگه میدارن — گاهی با هدفهایی مثل بارگذاری سریعتر یا دسترسی آفلاین، ولی خیلی وقتها هم ناخواسته و بیدلیل.
توی این مقاله، میخوایم با هم سرک بکشیم به پشت صحنهی مرورگرها و ببینیم این دادهها از کجا میان، چرا بعضیهاشون پاک نمیشن، و چطور میشه جلوی این هزینههای پنهان حافظه رو گرفت.
مرورگرهای امروزی وظایف خودشون رو بین چند «process» یا فرایند جدا تقسیم میکنن.
به عبارت ساده تر مرورگر امروزی یک برنامه ی چند بخشی هست، شامل یک بخش مدیریتی، چند بخش نمایش دهنده برای هر سایت، یک بخش گرافیکی و چند بخش جانبی.
در کنار این فرایند ها که هرکدام یک بخش مستقل از مرورگر هستند، چند «thread» وجود داره. تردها به بیان ساده رشته های کاری کوچکی هستن که میتونن کارها را به صورت همزمان انجام بدن.
همونطور که میشه حدس زد، این جداسازی و تقسیم بندی وظایف میتونه باعث سریعتر، امنتر و پایدارتر شدن مرورگر بشه. همین ویژگی امکان آزاد کردن حافظه در زمان بستن تبها رو هم فراهم میکنه.
این معماری به مرورگر اجازه میده بعد از بستن یک تب، کل فرآیند مربوطه را ببنده و حافظه رو فوراً آزاد کنه .
Browser process : بخش اصلی مرورگر که به سیستم عامل دسترسی داره و ظاهر مرورگر (نوار آدرس و دکمه ها) رو میسازه، «Browser process» هست. این بخش مثل یک مدیر عمل میکنه: تبها را باز و بسته میکنه، به حافظه سر میزنه، و بقیه ی بخشها را راه میاندازه.
تردهای مهم:
UI thread : این ترد مسئول واکنش به کلیکها، تاچ ها و تغییرات در ظاهر مرورگر هست. مثلاً اگر روی تب کلیک کنید یا تو نوار آدرس چیزی بنویسید، این ترد اون رو دریافت و پیگیری میکنه. چون رابط کاربر باید سریع باشه، این ترد نباید کار طولانی انجام بده.
I/O thread : این ترد کارهای ورودی/خروجی (مثل نوشتن فایل، خواندن از دیسک یا ارتباط با دیگر تردها) رو انجام میده. اگر مرورگر بخواد فایلی دانلود کنه یا به کارت شبکه پیام بفرسته، این ترد وارد عمل میشه.
Renderer process : هر تب جداگانه ایی که باز میکنید در یک «Renderer process» جداگانه نمایش داده میشه. تو این بخش، متن و کد ها خونده میشن، شکل و ظاهر صفحه ساخته میشه و برنامه های جاوااسکریپت اجرا میشن و صفحه وب برای کاربر نمایش داده میشه.
جدا کردن این فرایند برای هر تب باعث میشه اگر محتوای یک تب خراب بشه بقیه ی تب ها و مرورگر مشکل پیدا نکنند.
تردهای مهم:
Main thread : ترد اصلی مسئول خواندن HTML و CSS، ساختن DOM، اجرای جاوااسکریپت و تغییر صفحه است. در واقع مسئول تولید Render Tree و Paint instructions هست.
وقتی شما با صفحه تعامل میکنید (مثلاً روی دکمه ای در یک وبسایت کلیک میکنید)، این ترد باید کد مربوطه را اجرا کند و نتیجه را روی صفحه نشون بده. اگر کد JS سنگین و طولانی باشه، این ترد مشغول میشه و صفحه گیر میکنه؛ اینجاست که بهینه سازی کد های جی اس خیلی اهمیت پیدا میکنه.
Compositor thread : یک ترد مسئول گرفتن layers (لایه هایی که توسط main thread ساخته شدن)، و ترکیب (compositing) اونها و تولید تصویر نهایی هست.
وقتی اسکرول میکنی یا یک animation ساده (مثل transform: translateZ(0) یا opacity) داری، مرورگر میتونه بدون درگیر کردن Main Thread، فقط با GPU compositing اون رو رندر کنه. این باعث میشه که حتی اگه Main Thread مشغول اجرای JS سنگین باشه، اسکرول یا انیمیشن روان باقی بمونه (تا حدی). البته برای اینکه این بخش هایی که گفتیم توسط این ترد انجام بشه لازمه که انیمیشن یا اسکرول یا … یه شرایطی داشته باشه.
این دو ترد Main Thread و Compositor Thread موازی اجرا میشن. ولی Main Thread باید اول paint instructions رو تحویل بده تا Compositor بتونه لایه ها رو ترکیب کنه.
وقتی تغییری نیاز به layout یا paint جدید داره (مثلاً تغییر width یا background)، Main Thread باید کارش رو تموم کنه، بعد Compositor میتونه فریم رو بسازه.
و نکته بعدی اینکه وقتی تغییری فقط compositable باشه (مثل transform یا opacity)، مستقیم روی Compositor میافته و نیازی به Main Thread نداره.
Worker threads : مرورگرها این امکان رو دارن که جاوااسکریپت رو در تردهای پس زمینه اجرا کنن. مثل «Web Workers» که این تردها میتوانند محاسبات سنگین یا دانلودهای طولانی رو انجام بدن بدون اینکه ترد اصلی رو کند کنن. یک نوع دیگه از ترد های پس زمینه «Service Worker» ها هستن که بر خلاف Web Worker همیشه توسط خود کاربر اجرا نمیشه، مرورگر در پاسخ به event هایی مثل fetch، push یا sync خودش فعالش میکنه.
GPU process : برای اینکه انیمیشنها و فیلمها روانتر پخش بشن، مرورگر کارهای سنگین گرافیکی رو به یک بخش مجزا میسپاره که از کارت گرافیک استفاده میکنه. این بخش «GPU process» هست که خودش شامل چندین ترد موازیه، چون کارهای گرافیکی متنوع و زمانبر هستن، و باید با هم موازیسازی (parallelization) بشن.
Network process : برای اینکه مرورگر بتونه صفحات و فایلها رو دریافت کنه، باید به اینترنت متصل بشه — این یعنی کلی عملیات شبکهای مثل ارسال درخواستهای HTTP، دریافت پاسخ، مدیریت DNS، TLS و کوکیها.
در مرورگرهای قدیمی، تمام این کارها توسط همون Browser Process اصلی انجام میشد. نتیجهاش این بود که اگر مشکلی توی دریافت پاسخ، تأخیر تو DNS resolution یا باگ تو TLS negotiation پیش میاومد، ممکن بود کل مرورگر قفل کنه یا کرش کنه. حتی تبهایی که هیچ ربطی به اون درخواست نداشتن هم از کار میافتادن.
مرورگرهای مدرن برای حل این مشکل، بخش شبکه رو جدا کردن. حالا یک پردازش مستقل به نام Network Process مسئول انجام همهی کارهای شبکهایه. حالا اگر مشکلی توی ارتباط با اینترنت یا باگی در یکی از لایههای شبکه پیش بیاد، فقط همون پردازش crash میکنه — نه کل مرورگر. این یعنی مرورگر پایداری بیشتری داره و بقیه تبها به کارشون ادامه میدن.
Extensions و Utility processes: افزونه هایی که به مرورگر اضافه میکنید (مثل ابزارهای مدیریت رمز و بلاکر تبلیغات) هر کدام تو «Extension process» خودشون اجرا میشن تا امنیت حفظ بشه. همچنین برخی کارهای خاص مثل پخش صدا یا راه اندازی اولیه کتابخانه ها در «Utility process» های کوچک انجام میشن.
به زبان ساده مرورگر مثل یک راهنما، آدرس رو میگیره، مسیر را پیدا میکنه، آیتم های لازم (HTML، CSS، تصاویر) رو جمع میکنه، صفحه رو تشکیل میده، تزیینش میکنه و در پایان شما میتونید صفحه وب رو مشاهده کنید. ببینیم از لحظه ایی که URL در آدرس بار وارد میشه تا زمانیکه صفحه وب مشاهده میشه چه مسیری طی میشه.
۱. دریافت ورودی : وقتی شما آدرس یا عبارت جستجو را در نوار آدرس وارد میکنید UI thread ورودی را میخونه و تشخیص میده که باید به موتور جستجو ارجاع بشه یا به URL. (اگر ورودی شما شبیه یک آدرس اینترنتی معتبر باشه (مثل example.com یا شامل http:// یا https:// باشه)، مرورگر اون رو به عنوان URL در نظر میگیره. در غیر این صورت، مرورگر فرض میکنه که شما یک عبارت جستجو وارد کردید و اون را به موتور جستجوی پیشفرض (مثل Google یا Bing) ارسال میکنه. البته مرورگرهای مدرن مثل Chrome و Firefox از الگوریتمهای پیشرفته و حتی یادگیری رفتار کاربر برای این تصمیمگیری استفاده میکنن)
۲. حرکت به سمت مقصد : بعد از فشردن دکمه Enter یک درخواست شبکه توسط UI thread ایجاد میشه.
طی چند مرحله ساده Network process وظایف خودش رو به این شکل انجام میده:
DNS Lookup : مرورگر باید اول IP دامنه رو پیدا کنه.
TCP Connection : یک اتصال TCP به سرور برقرار میشه.
TLS Handshake : مرورگر و سرور با هم یک ارتباط امن رمزنگاری شده برقرار میکنن.
HTTP Request over TLS : مرورگر درخواست HTTP رو از داخل کانال امن TLS میفرسته (که این ترکیب همون HTTPS هست)
HTTP Response over TLS : سرور پاسخ رو هم از داخل همین کانال رمزنگاری شده برمیگردونه
Redirect Handling (اگر لازم باشه) : اگه پاسخ HTTP شامل redirect باشه، مرورگر URL جدید رو دنبال میکنه
.
در حالیکه این مراحل طی میشه پروسس های دیگری هم درگیر میشن:
۳. خواندن پاسخ : پیش از دریافت کامل پاسخ، مرورگر یک Renderer process آزاد پیدا یا ایجاد میکنه.
از Browser Process (که بالاتر مسئول مدیریت کلی تبها، آدرسها، درخواستها و فرآیندهاست) به Renderer Process (که مسئول رندر کردن و نمایش محتوای صفحه است) پیامی میفرسته که الان میتونیم به این صفحه بریم. اگر سرور بگوید «به آدرس دیگری برو»، مرورگر مسیرش را عوض میکند.
۴- دریافت پاسخ : سرور شروع به ارسال «بسته های اطلاعاتی» میکنه: HTML، فایلهای CSS، تصاویر و کدهای جاوااسکریپت. مرورگر این بسته ها رو دریافت میکنه و به پروسس های مختلف ارسال میکنه؛ مثلا متن HTML و CSS به Renderer process میره.
۵- ساختن اسکلت صفحه (Layout) : در این مرحله Renderer process اول HTML را میخونه و اندازه viewport محاسبه میکنه و بر این اساس یک «درخت» میسازه که DOM صفحه رو نشان میده. فایلهای CSS خوانده میشن و یک درخت دیگر ساخته میشه که استایل ها را مشخص میکنه (CSSOM). این دو با هم ترکیب میشن تا مشخص یشه هر عنصر در صفحه چطور باید نمایش داده بشه.
۸. چیدمان (Compositing) : اگر انیمیشن یا اسکرول وجود داشته باشه، بعضی عناصر صفحه تو لایه های جداگانه روی GPU رسم میشن و compositor thread لایه ها رو ترکیب و تصویر نهایی رو به GPU میفرسته.
تبدیل دادهها به پیکسلها در مرورگر:
ابتدا در Renderer Process شروع میشه (ساخت DOM، layout، paint)، بعد توسط Compositor Thread لایه ها ترکیب میشن، و در نهایت GPU Process دادهها رو به GPU واقعی میفرسته تا پیکسلها روی صفحه نمایش داده بشن.
۱۰. اجرای کد (JavaScript execution) : وقتی مرورگر داره HTML رو خط به خط میخونه و ساختار DOM رو میسازه، ممکنه برسه به تگ اسکریپت. مرورگر دست نگه میداره (Parsing HTML متوقف میشه). چون اجرای JavaScript میتونه محتوای صفحه رو تغییر بده. بعد از اجرای کامل کد جی اس، دوباره به parsing HTML ادامه میده. اگه فایل JavaScript حجم زیادی داشته باشه یا از سرور کندی بیاد، صفحه برای چند ثانیه ممکنه متوقف بمونه. در واقع، هیچ چیز دیگه ای رندر نمیشه تا وقتی که اجرای اون فایل تموم بشه. البته استفاده از defer و async کمک میکنه HTML بدون وقفه ادامه پیدا کنه و اسکریپتها به شکل بهینه تر اجرا بشن.
۱۱. پایان بارگذاری : بعد از بارگذاری کامل منابع و اجرای کدها، Renderer process یک پیام پایان بارگذاری را به Browser process ارسال میکنه. با این کانتکس که من همه چیو گرفتم، اجرا کردم، صفحه کامل آماده ست.
کاربرد این پیام اینه که Browser Process با دریافت این پیام میفهمه که: دیگه نیازی نیست منابع بیشتری برای این صفحه نگهداره و زمان مناسبیه برای:
اجرای افزونه ها (extensions)
اندازه گیری performance (مثل metrics مربوط به Load time)
فعالکردن بعضی رفتارها مثل prefetch یا prerender
حالا که مسیر اجرا و نمایش صفحات وب توی مرورگر رو بررسی کردیم، ببینیم مدیریت حافظه چطور انجام میشه و هزینه های پنهان حافظه کجا به حساب ما نوشته میشن.
همونطور که اشاره شد هر تب مرورگر، مدیریت حافظه ی خودش رو داره.
در سطح JavaScript Engine، دو مدل برای مدیریت حافظه وجود داره:
Stack : برای دادههای موقتی و کوچک، (مثل اعداد، بولینها، و متغیرهای مربوط به اجرای یک تابع).
به دلیل ماهیت ساده اش خودش آزاد میشه.
Heap : برای آبجکت ها، آرایه ها، نودهای DOM، و داده هایی که عمر بیشتری دارند. از دید JavaScript Engine، همیشه Memory Leak در Heap اتفاق میافته.
برای مدیریت حافطه در Heap بخشی در JavaScript Engine وجود داره به نام Garbage Collector. که خودش یکسری الگوریتم های هوشمند داره و به صورت async در پس زمینه اجرا میشه. علاوه بر این خودش در Main Thread اجرا میشه ولی سعی میکنه تا جای ممکن pause ایجاد نکنه.
کارش اینه که به طور دوره ای Heap را اسکن میکنه و دنبال آبجکتهایی میگرده که هیچ ارجاع فعالی (reachable reference) ندارن و وظیفه داره آبجکت هایی رو که دیگه هیچ ارجاعی به آنها وجود نداره آزاد کنه.
متوجه شدیم نشت حافظه معمولاً مثل یک باگ واضح خودش رو نشون نمیده، اما میتونه بهمرور زمان تجربهی کاربر رو بهشدت تحت تأثیر قرار بده.
با شناخت بهتر ابزارها و الگوهای درست، میتونیم این هزینههای پنهان رو به حداقل برسونیم، استاندارد کدمون رو بالا ببریم و در نهایت برنامهنویسهای حرفهایتری باشیم.
فراموشی clearTimeout, removeEventListener, abortFetch, unsubscribe
نگهداشتن Reference به آبجکتهایی که دیگه نیازی نیست
اگر از وانیلا جی اس استفاده میکنیم removeEventListener فراموش نکنیم
btn.removeEventListener('click', handleClick);
در React یا فریمورکها، استفاده از cleanup function
useEffect(() => { window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); // پاکسازی }; }, []);
استفاده از Delegation در بعضی موارد
اگه چند المنت مشابه داریم، به جای اینکه برای هرکدوم جداگانه addEventListener تعریف کنیم، میتونیم روی والدش یک لیسنر کلی بذاریم و با event.target تشخیص بدیم روی کدوم کلیک شده. از این طریق کمتر لیسنر ثبت میشه.
document.getElementById('comments').addEventListener('click', (event) => { const target = event.target if (target.matches('.like-btn')) { const id = target.dataset.id; likeComment(id); } });
تایمرها یا interval های رهاشده
setInterval, setTimeout هایی که پاک نمیشن، حتی اگه کامپوننت React یا فانکشن از بین بره، اینها هنوز در حافظه فعال می مونن و حتما باید از ()clearInterval و ()clearTimeout در فانکشن cleanup استفاده بشه.
استفاده نادرست از Map یا Set
مشکل: وقتی آبجکتهایی رو در Map یا Set نگهداری میکنیم در حافظه باقی می مونن تا زمانی که پاک بشن.
const map = new Map(); function saveMetadata(domElement) { map.set(domElement, { createdAt: Date.now() }); }
راه حل: استفاده از WeakMap بهجای Map زمانی که key شما یک object هست، کمک میکنه از memory leak جلوگیری بشه. چون WeakMap به صورت خودکار اجازه میده GC (Garbage Collector) اون key (و مقدار مربوط بهش) رو پاک کنه وقتی که هیچ reference دیگهای به اون object وجود نداره. این یکی از ترفندهای پیشرفته ولی بسیار کاربردی در مدیریت حافظه هست.
const metadata = new WeakMap(); function saveMetadata(domElement) { metadata.set(domElement, { createdAt: Date.now() }); }
وقتی یه تابع رو داخل تابع دیگه تعریف کنیم و اون به متغیرهای بیرونی دسترسی داشته باشه، احتمال داره باعث نشت حافظه بشه.
مثال:
function outer() { const largeData = new Array(1000000).fill('🔥'); return function inner() { console.log('Hello'); }; }
مشکل:
روی scope outer تابع inner برگردونده میشه و یه closure میسازه
تابع inner از نظر JS ممکنه به largeData نیاز داشته باشه بنابراین (Garbage Collector) GC پاکش نمیکنه
راه حل: scope بزرگ نساز — (separation of concerns)
function createInner() { return function inner() console.log('Hello'); }; } function doSomething() { const largeData = new Array(1000000).fill('🔥'); const inner = createInner(); return inner; }
اینجا inner فقط به اسکوپ createInner وصله (که کوچیکه) و largeData توی اسکوپ جداست و زودتر آزاد میشه.
نکته: از closure های بیدلیل اجتناب کنیم. بهتره که هر تابع فقط دادههای لازم رو نگه داره و تا جایی که میشه توابع رو کوچیک و جدا از هم تعریف کنیم.
وقتی اشتباهاً متغیری بدون let, const, var تعریف میکنیم، global میشه و تا زمانی که پاک نشه در حافظه باقی می مونه. البته دیگه این روزها همه جا از 'use strict' استفاده میشه که از بروز این مشکل جلوگیری میکنه.
در مورد تعریف متغیر با var هم احتیاط هایی لازمه. اگر هنوز از این متغییر استفاده میکنیم باید حواسمون باشه چون Function scope هست اگر خارج از تابع مربوطه تعریف بشه ممکنه به عنوان متغییر گلوبال در نظر گرفته بشه و در حافظه باقی بمونه.
مواردی که ذکر شد مموری لیک در سطح JavaScript Engine بودن. در سطح مرورگر، در بخش های دیگه هم نشت حافظه اتفاق میوفته که مباحث ادونس تری رو شامل میشه و در فرصت این مقاله نمی گنجه.
لیک در DOM Memory
لیک در GPU Memory
Image Cache → تصاویر آزاد نشده
اینها چیزهایی بودن که در مسیر مطالعه و بررسی دلایل و عوامل نشت حافظه یاد گرفتم. من فکر میکنم ما توسعه دهنده های سمت کلاینت، هر چقدر بیشتر بدونیم دقیقاً در پشتصحنهی مرورگر چه اتفاقی میافته؛ میتونیم اپلیکیشنهایی سریعتر، سبکتر و قابلاعتمادتر بسازیم.
از توجه شما خیلی ممنونم. خوشحال میشم اگه شما هم تجربهای در این زمینه داشتید یا با ابزارها و تکنیکهایی مواجه شدید که براتون مفید بوده، با من و بقیه به اشتراک بذارید. گفتوگوی ما میتونه به بهتر شدن کار همهمون کمک کنه 🌱