«صفحهآرایی» یا همان Layout فرآیندی است که در آن مرورگر مشخّصات هندسی عناصر را به دست میآورد: یعنی اندازهی آنها و مکان دقیقشان روی صفحه. بر اساس کدهای CSS نوشته شده، محتوای داخل عنصر، یا عنصرِ پدر، اندازهی عنصرها مشخّص میشود. این فرآیند در مرورگرهای Chrome، Opera، Safari و Internet Explorer به نام Layout معروف است و در فایرفاکس به آن Reflow میگویند.
مثل مرحلهی محاسبهی استایل، نگرانیهای اصلی در مورد هزینهی صفحهآرایی عبارتند از:
- تعداد عناصری که آرایششان در صفحه باید مشخّص شود.
- پیچیدگی صفحهآرایی.
- در حالت عادی صفحهآرایی همهی عناصر صفحه را در بر میگیرد.
- تعداد عناصر موجود در صفحه در عملکرد تأثیر دارد؛ باید تا جایی که امکان دارد جلوی اتّفاق افتادن مرحلهی Layout را بگیرید.
- عملکرد مدل صفحهآرایی را ارزیابی کنید. فلکسباکس جدید معمولاً سریعتر از نسخهی قدیمی فلکسباکس یا مدلهای صفحهآرایی با float است.
- جلوی «همگامسازی اجباری صفحهآرایی» و «Layout thrashing» را بگیرید؛ ابتدا مقادیر استایلی را بخوانید و بعد تغییرات استایلی را انجام دهید.
تا جایی که ممکن است جلوی صفحهآرایی را بگیرید
وقتی استایلها را تغییر میدهید مرورگر بررسی میکند که آیا این تغییرات نیاز به انجام صفحهآرایی دارد یا نه. تغییر «ویژگیهای هندسی» مثل width
، height
، left
یا top
همیشه باعث اجرای مرحلهی صفحهآرایی میشود.
.box {
width: 20px;
height: 20px;
}
/**
* تغییر عرض و ارتفاع
* خواهد شد layout باعث اجرای
*/
.box--expanded {
width: 200px;
height: 350px;
}
در بیشتر مواقع، صفحهآرایی کلّ صفحه را در بر میگیرد. اگر تعداد عناصر موجود در صفحه زیاد باشد، زمان زیادی برای به دست آوردن مکان و مختصّات همهی آنها طول میکشد.
اگر نمیتوانید از صفحهآرایی مجدّد جلوگیری کنید، از DevTools مرورگر کروم استفاده کنید تا ببینید مرحلهی صفحهآرایی چه قدر طول میکشد و آیا مسئول کُندشدن صفحه، همین است یا نه.
به جای مدلهای قدیمیِ صفحهآرایی از Flex Box یا CSS Grid استفاده کنید
در دنیای وب روشهای مختلفی برای چیدمان صفحه وجود دارد، بعضی از این روشها پشتیبانی بهتری در مرورگرهای مختلف نسبت به روشهای دیگر دارند. در مدل قدیمی صفحهآرایی میتوان مکان عناصر را با بعضی از ویژگیهای css مثل position و float مشخّص کرد.
تصویر زیر هزینهی صفحهآرایی را روی ۱۳۰۰ عنصر با روش float نشان میدهد. البته در دنیای واقعی این گونه نیست، چون در بیشتر اپلیکیشنها به صورت ترکیبی از روشهای مختلف صفحهآرایی استفاده میشود و هیچ وقت چیدمان همهی عناصر صفحه با float انجام نمیشود.

حالا اگر در این مثال به جای float از فلکسباکس استفاده میکردیم، تصویر متفاوتی را میدیدیم:

حالا ما با اینکه تعداد عناصر موجود در صفحه را تغییر ندادیم، و چیزی که روی صفحه میبینیم همان قیافهای را دارد که در حالت قبل میدیدیم، به شدّت در زمان صرفهجویی کردیم (در اینجا ۳.۵ میلیثانیه در مقابل ۱۴میلیثانیه). البته شاید بعضی وقتها نتوان از فلکسباکس استفاده کرد. به خاطر اینکه پشتیبانی ضعیفتری نسبت به float دارد. امّا تا جایی که میتوانید حداقل در مورد تأثیر مدلهای صفحهآرایی در عملکرد سایت تحقیق کنید، و مدلی را انتخاب کنید که انجام آن کمترین هزینه را داشته باشد.
به هر حال چه فلکسباکس را انتخاب کنید چه نکنید، در نقاط پرفشار و حسّاس برنامهی خود سعی کنید اصلاً صفحهآرایی انجام نشود!
از همگامسازی اجباری صفحهآرایی جلوگیری کنید
همان طور که قبلاً گفتیم برای اینکه یک فریم تولید شود، باید این پنج مرحله به ترتیب اجرا شود:

ابتدا کد جاوااسکریپت اجرا میشود، سپس محاسبهی استایل، و بعد صفحهآرایی انجام میشود. امّا این امکان وجود دارد که مرحلهی صفحهآرایی قبل از مرحلهی اجرای JavaScript انجام شود، که به آن forced synchronous layout یا «همگامسازی اجباری صفحهآرایی» میگوییم.
اوّل از همه باید بدانید کد جاوااسکریپتی که اجرا میشود تمام مقادیر مربوط به صفحهآرایی در فریم قبلی را میداند و آنها را در دسترس شما میگذارد. برای همین وقتی که مثلاً میخواهید ارتفاع یک عنصر را در صفحه نمایش دهید، میتوانید کدی مثل این بنویسید:
// تنظیم کردیم که دستورات ما در آغاز فریم بعدی اجرا شوند
requestAnimationFrame(logBoxHeight);
function logBoxHeight() {
// ارتفاع عنصر را میگیریم و در کنسول نمایش میدهیم
console.log(box.offsetHeight);
}
مشکل وقتی به وجود میآید که بخواهید قبل از خواندن مقدار ارتفاع (در اینجا با offsetHeight
) استایل عنصر را تغییر دهید:
function logBoxHeight() {
box.classList.add('super-big');
// ارتفاع عنصر را در واحد پیکسل میگیریم
// و آن را در کنسول نمایش میدهیم.
console.log(box.offsetHeight);
}
حالا مرورگر برای اینکه بتواند ارتفاع درست عنصر را به ما بدهد، مجبور است که اوّل کلاس super-big را به آن اعمال کند (به خاطر اینکه قبل از گرفتن offsetHeight
کلاس super-big را به آن اضافه کردیم). برای این منظور اجرای کد جاوااسکریپت متوقّف میشود، سپس مرحلهی محاسبهی استایل اجرا میشود، و بعد صفحهآرایی انجام میشود. فقط در این صورت است که مرورگر میتواند ارتفاع درست عنصر را به ما بدهد. حالا به مرحلهی اجرای کد JavaScript بر میگردد و ادامهی آن را اجرا میکند. این کار در اینجا غیر ضروری و پرهزینه است.
به همین دلیل، شما باید اوّل هر چیزی را که لازم دارید بخوانید (که مرورگر از مقادیر موجود در فریم قبلی استفاده کند)، و بعد تغییرات ظاهری مورد نظرتان را اعمال کنید.
همان تابع قبلی را که اصلاح کنیم به این شکل در میآید:
function logBoxHeight() {
// ارتفاع عنصر را در واحد پیکسل میگیریم
// و آن را در کنسول نشان میدهیم
console.log(box.offsetHeight);
box.classList.add('super-big');
}
خیلی وقتها واقعاً نیازی به این نیست که ابتدا استایل اعمال کنید و بعد مقادیر را بخوانید؛ و استفاده از همان مقادیر موجود در فریم قبل کافیست. اجرای مراحل محاسبهی استایل و صفحهآرایی قبل از اینکه مرورگر دوست داشته باشد آنها را انجام دهد باعث ایجاد تنگنا میشود، و این چیزی نیست که در حالت عادی میخواهید.
از Layout thrashing جلوگیری کنید
در یک حالت همگامسازی اجباری صفحهآرایی خیلی بدتر میشود: آن حالت وقتی است که تعداد زیادی همگامسازی اجباری را پشت سر هم به سرعت انجام دهید. مثل این:
function resizeAllParagraphsToMatchBlockWidth() {
// مرورگر را در یک چرخهی خواندن-نوشتن-خواندن-نوشتن قرار میدهد
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
در این کد روی تعدادی پاراگراف حلقه زدیم و عرض هر پاراگراف را با عرض عنصری که box نامیده شده است منطبق میکنیم. ظاهراً بیخطر است، ولی اگر دقّت کنید میبینید در هر دور حلقه، ابتدا یک مقدار استایلی خوانده میشود (یعنی box.offsetWidth
) و به سرعت از این مقدار برای تعیین عرض پاراگراف استفاده میشود (paragraphs[i].style.width
). در دور بعدی حلقه وقتی قرار است offsetWidth
مجدداً خوانده بشود، مرورگر میفهمد نسبت به دفعهی قبل که offsetWidth
را خوانده بود یک تغییر استایلی داشتهایم (چون در دور قبلی حلقه، عرض پاراگراف را تعیین کرده بودیم)، بنابراین باید این تغییر استایلی را اعمال کند و مجدّداً صفحهآرایی را انجام دهد. این قضیه در هر دور حلقه اتّفاق میافتد!
در این مثال میتوان مشکل را به شکل زیر حل کرد:
// ابتدا خواندن
var width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
// حالا نوشتن
paragraphs[i].style.width = width + 'px';
}
}
برای اطمینان از اینکه هیچوقت چنین مشکلاتی پیش نیاید میتوانید از کتابخانهی FastDOM استفاده کنید، که به صورت خودکار عملیّاتهای خواندن و نوشتن شما را دستهبندی میکند، و از اتّفاق افتادن ناخواستهی «همگامسازی اجباری صفحهآرایی» یا Layout thrashing جلوگیری میکند.