فرایند نمایش یک صفحه وب روی مرورگر، یک فرایند بسیار جالبه. مراحل زیادی داره و توی هر کدوم از این مراحل اتفاقات خیلی هیجان انگیزی رخ میده که اگر اون ها رو بدونیم میتونیم کاری کنیم که توی Performance اپلیکیشن و تجربه کاربر از کار کردن با سرویس مون تاثیر بسیار زیادی داشته باشه.
راستش من با این سوال تو یکی از مصاحبه های اخیرم برخورد کردم و مطلبی روی مدیوم پیدا کردم که خیلی با جزییات و کامل توضیح داده بود که انتهای مطلب لینکش رو قرار میدم که حتما اون رو بخونید. من چیزی که خودم ازش فهمیدم رو اینجا با شما به اشتراک میذارم و مساله و کاربرد هاش رو سعی میکنم توضیح بدم تا براتون شفاف تر باشه. موضوع خیلی مهم و جذابیه و مطلب هم طولانی پس حوصله کنید که خیلی چیز قراره یاد بگیریم.
اصطلاحاتی مثل DOM که زیاد شنیدید، CSSOM که شاید کمتر شنیده باشید، Render Tree که شاید نشنیده باشید و CRP که شاید اصلا نشنیده باشید! اینجا در مورد همه شون حرف میزنیم و میبینیم هر کدوم کجا وارد عمل میشن و توی هر کدوم چه اتفاقاتی میوفته!
به سوالاتی مثل اینکه چرا این صفحه کنده؟ چرا CSS هاش با تاخیر لود میشن؟ چرا فونت اش بعد از یک تاخیری ظاهر میشه؟ چرا وقتی بعضی اکشن ها اتفاق میوفته صفحه لگ میخوره؟ (برای درک بهتر این مسائل اینجا رو بخونید)
یا از طرف دیگه، چطور این صفحه انقدر روون اجرا میشه؟ چطور انقدر پرفورمنس این سایت خوبه و خیلی سوالات دیگه شاید بعد از خوندن این مطلب و رسیدن به جواب هاشون براتون کاملا روشن بشه.
شروع این داستان از یک درخواست شروع میشه، درخواستی که به یک سرور (یا فایل لوکال) ارسال میشه و سرور جواب شما رو با یک صفحه HTML میده. ولی دقیقا چه اتفاقی میوفته؟ وقتی شما درخواست رو ارسال میکنید مرورگر شما یک header به اسم accept همراه درخواست ارسال میکنه به سرور و به سرور اطلاع میده که توانایی هندل کردن چه نوع فایل هایی (MIME Type) هایی رو داره.
سرور درخواست صفحه رو میگیره و بعد از تمام اتفاقاتی که اونطرف میوفته یک داکیومنت HTML رو به سمت کلاینت ارسال میکنه که در واقع یک Binary Stream Format هست.
توی هدر response سرور کلید دیگه ای وجود داره به اسم Content-Type که در واقع به مرورگر میگه که چه نوع MIME Type ای رو ارسال کرده که اینجا میشه `text/html` و با یک انکدینگ خاص مثل UTF-8 انکد شده و از این طریق مرورگر میدونه که بر اساس چه کارکاکتر ستی باید این اطلاعات رو دیکد کنه که در نهایت اون رو تبدیل کنه به یک فایل text که بتونه مراحل بعدی رو انجام بده.
برای دیدن این فرایند میتونید به کمک DevTools مرورگر (inspector خودمونیش) ببینید این فرایند رو. داخل تب network بشید و بر اساس Doc درخواست ها رو فیلتر کنید و به یه صفحه ای درخواست ارسال کنید و ببینید همه این ها رو:
خب تا اینجا مرورگر یه فایل text بی معنی داره، تو یک حالت مینیمال یه فایل شبیه این میشه:
مثال ها رو من از سورس منبع آوردم.
همینطور که مشخصه این فایل داخلش یه فایل CSS و Javascript آدرس داده شده، که بعدا بهش میرسیم. اگه این فایل رو توی مرورگر ببینیم احتمالا چیزی شبیه این میشه:
بریم ببینیم مرورگر چه مراحلی رو طی میکنه تا همین کار به ظاهر ساده رو انجام بده.
مرورگر فایل HTML رو میخونه و هرجا که با یک تگ برخورد میکنه یه آبجکت جاواسکریپت میسازه به اسم Node البته بر اساس اینکه این تگ چه نوع تگی باشه این آبجکت هم متفاوته. همه کلاس های جاواسکریپت مربوط به تگ ها در واقع از کلاس Node ارث بری میکنند.
نکته: هرجا که [native code] میبینید در واقع کدی هست که با زبون های سطح پایین مثل ++C نوشته شده پس نگران اون نباشید.
بعد از اینکه از همه المنت های HTML یک آبجکت جاواسکریپتی ساخته شد، از اونجا که ساختار فایل های HTML به صورت nested و تو در تو هست مرورگر باید همون ساختار رو ایجاد کنه.
پس بنابراین یه ساختار درختی از تمام Node هایی که داخل داکیومنت وجود داشت به کمک آبجکت هایی که ایجاد شده ساخته میشه. DOM در واقع یک WebAPI هست که به جاواسکریپت اجازه میده که با صفحه ارتباط برقرار کنه وگرنه خود جاواسکریپت درکی از DOM نداره. و این خیلی نکته ظریف و قشنگیه چون همه المنت هایی جاواسکریپت باهاشون تعامل برقرار میکنه صرفا تعدادی کلاس و آبجکت هستند.
در نهایت ساختار DOM Tree چیزی شبیه این خواهد بود:
دقت کنید که توی این ساختار درختی زیرمجموعه تگ p و h1 در واقعیت یک DOM Node نیستند، لزوما میتونه اینطور نباشه. DOM برای اینکه متن داخل تگ ها، کامنت های توی کد و atrribute ها و چندتا چیز دیگه رو مدیریت کنه از تایپ های خاص تری استفاده میکنه که میتونید لیستش رو اینجا ببینید.
اگر میخواید دقیقا ببینید که این DOM Tree چیه همین الان روی صفحه کلیک راست کنید و inspect بزنید و از این طریق ببینید این متنی که اینجاست چیه:
این دقیقا همون سورس کدی هست که به صورت computed توی مرورگر شما داره کار میکنه. حتی میتونید تغییرش بدید و تغییراتش رو توی همون لحظه روی صفحه ببینید.
بعد از اینکه مرورگر DOM Tree رو ایجاد کرد حالا وقتشه که استایل های هر تگ رو بهش اختصاص بده. تنها راهی که برای استایل دهی به المان های صفحه وجود داره استفاده از CSS هست. به کمک CSS Rule ها که تشکیل میشه از دو قسمت:
h1, h2 { color: red; font-size: 18px; }
قسمت اول که در واقع selector هست که اشاره میکنه به المان ها، حالا یا این کارو از طریق اسم تگ ها انجام میده یا آیدی شون یا کلاسشون یا حالت های دیگه. در واقع این سلکتور ها همون آبجکت های داخل DOM Tree رو هدف قرار میدن.
قسمت دوم استایل های هر کدوم از این المان هاست، که به صورت جفتی مورد استفاده قرار میگیره، مثلا color: red یا font-size: 18px. این مقادیر در حقیقت پراپرتی های هر کدوم از اون آبجکت های DOM رو تغییر میدن.
نکته: دقت کنید که پروسه استایل دادن به المان ها از روش های مختلفی امکان پذیره، میتونه به صورت یک فایل مجزا باشه، میتونه به صورت embed داخل همون document باشه، میتونه به صورت inline توی همون تگ تعریف بشه یا از طریق Javascript انجام بشه.
فرض کنیم برای اون صفحه این استایل ها رو داریم: (مجدد لازمه بگم که مثال ها رو از منبع آوردم)
بعد این که درخت DOM ساخته شد، مرورگر همه CSS ها رو از تمام سورس ها که قبل تر اشاره کردم میخونه و CSSOM رو میسازه. نکته ای که اینجا وجود داره اینه که بعضی المان ها هیچ استایلی ندارن(ولی توی DOM هستند)، پس تو این حالت چیکار میکنه؟ در واقع هر مرورگری یه سری استایل دیفالت و پیشفرض داره که میشه User Agent Stylesheet که از اونها استفاده میکنه. این مقادیر دیفالت از استاندارد های W3C هستند. مثلا اگر شما به یک المان font-size ندید از مقدار پیشفرض استفاده میکنه یا مقدار اولیه اش رو یک مقدار خنثی قرار میده.
هر Node داخل CSSOM اول استایل های پیشرفض UserAgent رو میگیره و بعد استایل هایی که اپلیکیشن شما داره روی اونها Override میشه. بعضی از المان ها هم ممکنه به خاطر ویژگی های خاصی که دارند بعضی استایل ها رو از والد شون به ارث ببرن. از این طریق Node های CSSOM به وجود میان. اگر میخواید دقیق تر ببینید این ارث بری چطور اتفاق میوفته میتونید اینجا رو ببینید.
موضوعی که در مورد این ارث بری خیلی حائز اهمیته اینه که به خاطر این ساختار Nested و تو در تویی که دارن، بعضی از پراپرتی ها همیشه از والد به ارث میرسن مثل font-size. برای همین هم هست که بهش میگیم Cascading Style Sheets یا همون CSS و دقیقا برای همین هست که نیازه دقیقا مثل DOM، مرورگر یک CSSOM هم ایجاد کنه.
ویژگی خاصی که CSSOM نسبت به DOM داره اینه که تگ هایی که استایلی ندارن یا بهتره بگم اصلا دیده نمیشن هیچوقت، مثلا script یا meta یا چیز هایی شبیه این ها، اصلا داخل درخت CSSOM تعریف نمیشن و حضور ندارند. منطقی هم هست چون لزومی نداره که باشن.
میتونید همه این کار ها که گفتم رو از طریق DevTools مرورگرتون ببینید:
در نهایت طبق مثال قبلی، درخت CSSOM چیزی میشه شبیه این:
همینطور که میبینید المان هایی که روی صفحه نمایش داده نمیشن، مثل link و script و ... داخل CSSOM حضور ندارن و پراپرتی هایی که با رنگ قرمز مشخص شدن (البته رنگش یکم نامشخصه) از والدشون به ارث رسیده.
بعد از اینکه اون دو تا درخت DOM و CSSOM ایجاد شدند مرورگر از ترکیب این دو تا درخت جدیدی میسازه به اسم Render Tree. مرورگر برای اینکه بتونه تمام المان های قابل نمایش رو روی صفحه بچینه (Layout) و بعد بتونه اون ها رو ترسیم کنه (Paint) به این ساختار درختی نیاز داره.
همینطور که از تصویر هم مشخصه بعضی المان ها اصلا داخل درخت نهایی وجود ندارند، این اتفاق چند دلیل داره. المان هایی که روی صفحه هیچ پیکسلی رو اشغال نمیکنن دلیل اول هست. مثلا p به خاطر display: none دلیلی برای حضور توی Render Tree نداره.
دلیل دوم رو قبلا هم مختصر گفتم، تگ هایی مثل script و link و ... هم اصلا قابلیت نمایش ندارند! پس بنابراین حضور اونا هم توی Render Tree بی معنیه.
شاید براتون سوال پیش بیاد که اگر مثلا یک المان خاص از visibility: hidden یا opacity: 0 استفاده میکنه هم آیا داخل Render Tree نمیاد؟ جواب این سوال خیر هست. چون اگر این پراپرتی ها رو امتحان کنید میبینید که از صفحه فضا رو اشغال میکنند ولی صرفا فقط قابل مشاهده نیستند. معیار Render Tree میزان پیکسل هایی هست که روی صفحه باید به ازای هر المان Paint بشه.
برخلاف DOM که میتونستید به کمک DevTools ساختارش رو مشاهده کنید، این امکان برای CSSOM وجود نداره. صرفا میتونید ببینید که هر کدوم از شاخه های DOM چه خصوصیات CSS رو دارند.
میتونید بعدا استایل هر کدوم از المان ها که دوست دارید رو به کمک JavaScript تغییر بدید که یه مطلب خوب و کامل توی CSS Tricks هست که میتونید اینجا بخونیدش و کلی چیز یاد بگیرید. از طرفی یک API جدید معرفی شده که کار استایل دادن با JS رو صفحه رو هندل میکنه که خیلی جذابه به اسم CSS Typed Object که اونم ارزش خوندن داره.
تا اینجا فقط متوجه شدیم که مرورگر چطور یه داکیومنت رو مدل سازی میکنه و چه فرایندی رو طی میکنه تا از یه text برسه به یه مدل استاندارد که بتونه رندرش کنه. در ادامه ببینیم که فرایند رندر کردن چطور صورت میگیره.
بعد از اینکه درخت های DOM و CSSOM و نهایتا Render Tree ایجاد شدند، مرورگر شروع میکنه تمام المان ها رو تک تک روی صفحه چاپ کردن.
این کار توی چند مرحله اتفاق میوفته:
عملیات چیدمان (Layout)
توی این مرحله مرورگر میاد اندازه هر کدوم از Node های داخل Render Tree رو محاسبه میکنه (بر حسب پیکسل) و همچنین موقعیت مکانی اون المان داخل صفحه رو هم در نظر میگیره (position). به این فرایند Reflow هم میگن (توی فایرفاکس).
علاوه بر این ها زمانی که صفحه Resize میشه یا Scroll هم میشه اتفاق میوفته. خیلی مهمه که بدونیم دقیقا چه زمان هایی این اتفاق میوفته چون بر این اساس میتونیم CSS های بهتری برای اپلیکیشن بنویسیم که تا جای ممکن فرایند layout روشون صورت نگیره، چون این عملیات کار سنگین و زمانبری هست و از نظر performance روی اپلیکیشن تاثیر میذاره.
اینجا لیست کاملی از همه چیز هایی که باعث میشن این عملیات دوباره اتفاق بیوفته رو میتونید ببینید که بر اساس اون سعی کنید کدی بنویسید که کمتر صفحه رو دچار تکرار Layout بکنه. چون قطعا روی FPS سایت تاثیر مستقیم میذاره و تجربه کاربری رو خراب میکنه.
اینجا هم یه مطلب خوب هست که توضیح داده چطور میتونیم طوری اپلیکیشن رو توسعه بدیم که تعداد Reflow ها رو توی اپلیکیشن به حداقل برسونیم. همچنین در مورد Layout Trashing هم صحبت کرده. اطلاعات واقعا ارزشمندی داره توصیه میکنم حتما بخوندیش.
عملیات (Paint)
یکی از جالب ترین مراحل تو پروسه نمایش یک صفحه این مرحله است! اگر با فتوشاپ کار کرده باشید میدونید که فتوشاپ از یه سیستم Layering استفاده میکنه که هر کدوم از لایه ها میتونن یه المان یا تصویر گرافیکی باشن که از ترکیب شدنشون یک تصویر کامل بوجود بیاد.
شبیه چنین اتفاقی دقیقا چیزی هست که توی مرورگر هم میوفته. بعد از اینکه مرورگر متوجه شد که چه چیزی رو باید کجای صفحه و با چه اندازه ای نشون بده (همون Layout که گفتم) نوبت اینه که بیاد اون ها رو ترسیم کنه.
دلیل اینکه این Layering هم اتفاق میوفته اینه که خیلی از المان های صفحات وب دقیقا مشابه اتفاقی که توی فتوشاپ میوفته روی هم قرار میگیرن و یه تصویر کلی رو میسازن. این المان ها ممکنه بر اساس اتفاقای تغییر شکل، تغییر رنگ یا تغییر موقعیت بدن (مثل انیمیشن ها) پس مرورگر باید قادر باشه این کار رو به صورت بهینه و با پرفورمنس خوب هندل کنه. این Layering به مرورگر این اجازه رو میده که این عملیات رو راحت تر و با سرعت بیشتری انجام بده.
مرورگر طبیعتا همه این لایه ها رو در آن واحد انجام نمیده و ممکنه براشون تقدم و تاخر قائل بشه. مرورگر پیکسل به پیکسل شروع به ترسیم میکنه و چیز هایی مثل رنگ آمیزی بگکراند ها، سایه ها، حاشیه ها (border) متون و ... رو انجام میده. به این کار Rasterizatoin میگیم. برای پرفورمنس بهتر ممکنه مرورگر این کار رو روی چند تا Thread مختلف انجام بده.
خب اینکار توسط CPU صورت میگیره که فرایند سنگین و زمانبریه ولی تکنیک جدید تری برای اینکار هست که توسط GPU یا همون کارت گرافیک تون انجام میشه که تاثیر خیلی زیادی توی پرفورمنس میذاره. این مطلب خیلی خوب این موضوع رو توضیح داده و باز کرده.
اگر دوست دارید این layering رو به چشم ببینید، میتونید توی Devtools مرورگرتون (کروم یا فایرفاکس) از گزینه More Tools استفاده کنید:
البته دقت کنید که همه این اتفاقات افتاده ولی هنوووووز یک پیکسل هم روی صفحه نمایش داده نشده. این اتفاق کی میوفته؟ توی مرحله بعد.
عملیات ترکیب (Composition)
خب بعد از اینکه DOM ایجاد شد، بعد از اینکه CSSOM بر اساس استایل های User Agent و استایل های inline و embed و همه سورس ها ساخته شد، بعد از اینکه بر اساس قابل نمایش بودن و نبودن هر المان به همراه استایل هاشون Render Tree ایجاد شد، بعد از اینکه موقعیت مکانی هر المان به همراه سایزشون در نظر گرفته شد و بعد از اینکه ویژگی های بصری هر کدوم از این ها محاسبه و Layering صورت گرفت....
حالا مرورگر همه این لایه ها رو در قالب تصاویر بیت مپی به ترتیب برای GPU ارسال میکنه که روی صفحه نمایششون بده.
فرستادن تمام لایه ها به یکباره برای GPU طبیعتا کار بهینه ای نیست چون که هر بار که یک اتفاق Reflow یا Repaint صورت میگیره باید انجام بشه برنابراین هر لایه به کاشی های کوچک تری شکسته میشه و بعد روی صفحه نمایش داده میشه. میتونید این کاشی ها رو هم توی DevTools مشاهده کنید.
به کل این فرایند میگن CRP یا Critical Rendering Path
برای این قسمت فکر کنم مطلب انقدر زیاد هست که دیگه حوصله خوندن ادامه شو نداشته باشید. توی قسمت بعدی این مطلب توضیح میدم که Browser Engine چطور کار میکنه و فرایند ساختن DOM و CSSOM به چه شکل انجام میشه. همچنین یاد میگیریم که چه فرایند هایی ممکنه باعث بشن این اتفاق کند تر بیوفته یا اون رو متوقف کنه و چندتا benchmark هم میگیریم که ببینیم مرورگر رفتارش در مقابل بعضی اتفاقات چطوریه.
خیییلی هیجان انگیزه.
امیدوارم از خوندن این مطلب لذت برده باشید، اگر دوست داشتید میتونید از طریق لینکدین با من در تماس باشید! حتما دوست دارم نظرتون رو راجع به این مطلب بدونم، اگر احساس کردید جایی چیزی از قلم افتاده یا درست بیان نشده بهم بگید.