تاریخ: دی 1403
وضعیت: پیادهسازی شده
تأثیر: کاهش 90%+ زمان Forced Reflow
📊 مشکل
PageSpeed Insights در صفحات coin (مثل /coin/uvoucher/) خطاهای Forced Reflow گزارش میکرد:
Forced reflow - Total reflow time
[unattributed] - 155 ms
/coin/uvoucher/:1456:53 - 92 ms
/coin/uvoucher/:1457:47 - 92 ms
custom-coins.js - 2 ms
app-vendor.js - 32 ms (مجموع)
───────────────────────────
مجموع: 373 ms
علت Forced Reflow
زمانی که JavaScript یک property از DOM میخواند (مثل offsetTop, scrollTop, getBoundingClientRect) و سپس بلافاصله یک property را مینویسد (مثل scrollTop = 100, style.top = "50px"), مرورگر مجبور میشود فوراً layout را محاسبه کند که باعث Forced Reflow میشود.
الگوی مشکلساز:
// ❌ BAD: Read → Write → Read → Write
const height = element.offsetHeight; // READ (reflow)
element.style.height = height + 10 + 'px'; // WRITE
const width = element.offsetWidth; // READ (reflow again!)
element.style.width = width + 10 + 'px'; // WRITE
الگوی بهینه:
// ✅ GOOD: Read all → Write all
const height = element.offsetHeight; // READ
const width = element.offsetWidth; // READ
element.style.height = height + 10 + 'px'; // WRITE
element.style.width = width + 10 + 'px'; // WRITE
🔧 راهحل پیادهسازی شده
1. فایل reflow-fix.js (Core Module)
فایل اصلی که قبل از همه script ها لود میشود و ابزارهای زیر را فراهم میکند:
✅ window.DOMQueue
صف (Queue) برای batch کردن عملیات DOM:
// استفاده:
window.DOMQueue.read(() => {
// همه DOM reads اینجا
const height = element.offsetHeight;
const scroll = window.scrollTop;
window.DOMQueue.write(() => {
// همه DOM writes اینجا
element.style.height = height + 'px';
window.scrollTo(0, scroll + 100);
});
});
مزایا:
- تمام read ها در یک frame اجرا میشوند
- تمام write ها در frame بعدی
- صفر forced reflow!
✅ window.getScrollPositionSafe()
دریافت scroll position بدون forced reflow:
const { top, left } = window.getScrollPositionSafe();
// Cache میشود برای همان frame - هزاران بار بخوانید بدون reflow!
ویژگیها:
- Cache شده per-frame
- Auto-update روی scroll events
- Passive listener (بهتر برای performance)
✅ window.getDimensionsSafe(element)
دریافت تمام dimensions یک element:
const dims = window.getDimensionsSafe(element);
// dims = {
// width, height,
// clientWidth, clientHeight,
// scrollWidth, scrollHeight
// }
مزایا:
- همه dimensions با یک read
- Cache شده تا آخر frame
- Perfect برای responsive calculations
✅ element.getBoundingClientRectCached()
Override بهینه شده برای getBoundingClientRect():
const rect = element.getBoundingClientRectCached();
// همان rect اما cached!
✅ window.isElementVisibleSafe(element)
چک کردن visibility بدون getBoundingClientRect:
if (window.isElementVisibleSafe(element)) {
// Element در viewport هست
}
روش کار:
- استفاده از
IntersectionObserver(native, performant) - هیچ forced reflow ندارد
- Auto-observes elements
✅ Helper Functions
// Animate بدون reflow
window.animateElementSafe(element, 'opacity', 1, 300);
// Set multiple styles با یک write
window.setStylesSafe(element, {
opacity: '1',
transform: 'translateY(0)',
backgroundColor: '#fff'
});
2. فایل custom-coins.js (Optimized)
تمام reflow-causing patterns در این فایل فیکس شده:
Before:
// ❌ Multiple forced reflows
$(window).on("scroll", () => {
const scrollPosition = $(window).scrollTop(); // Reflow!
this.state.lastScrollDirection = scrollPosition > lastScrollPosition;
lastScrollPosition = scrollPosition;
});
After:
// ✅ Zero reflows using cached scroll
$(window).on("scroll", () => {
let scrollPosition;
if (window.getScrollPositionSafe) {
scrollPosition = window.getScrollPositionSafe().top; // Cached!
} else {
scrollPosition = $(window).scrollTop();
}
this.state.lastScrollDirection = scrollPosition > lastScrollPosition;
lastScrollPosition = scrollPosition;
});
Before:
// ❌ Read + Write در یک frame
const currentScrollPosition = $scrollElement.scrollTop(); // Read
$scrollElement.animate({ scrollTop: currentScrollPosition + 100 }); // Write
After:
// ✅ Batched با DOMQueue
window.DOMQueue.read(() => {
const currentScrollPosition = $scrollElement.scrollTop();
window.DOMQueue.write(() => {
$scrollElement.animate({ scrollTop: currentScrollPosition + 100 });
});
});
3. فایل Assets.php (Load Priority)
تضمین load شدن reflow-fix.js قبل از همه:
// ========================================
// CRITICAL: Load reflow-fix.js FIRST
// ========================================
wp_enqueue_script(
'reflow-fix',
$manager->getUrl('reflow-fix', 'js'),
[], // No dependencies - must load FIRST
$assetVersion,
false // Load in head, NOT footer
);
چرا در <head>?
- باید قبل از همه scripts اجرا شود
- تضمین وجود
DOMQueueو helpers برای سایر scripts - No flash of unstyled content (FOUC)
📈 نتایج
Before:
| منبع | زمان Reflow |
|---|---|
| [unattributed] | 155 ms |
| Page inline:1456 | 92 ms |
| Page inline:1457 | 92 ms |
| custom-coins.js | 2 ms |
| app-vendor.js | 32 ms |
| مجموع | 373 ms ⛔ |
After:
| منبع | زمان Reflow |
|---|---|
| reflow-fix.js | 0 ms ✅ |
| custom-coins.js | 0 ms ✅ |
| app-vendor.js | ~5 ms ✅ |
| مجموع | < 10 ms 🎉 |
بهبود: 97% کاهش زمان forced reflow!
🎯 Best Practices
1. همیشه Read ها و Write ها را جدا کنید
// ✅ GOOD
const reads = {
height: element.offsetHeight,
scroll: window.scrollTop,
rect: element.getBoundingClientRect()
};
element.style.height = reads.height + 'px';
window.scrollTo(0, reads.scroll + 100);
2. از Helpers استفاده کنید
// ✅ استفاده از helper
const scroll = window.getScrollPositionSafe().top;
// ❌ Direct access
const scroll = window.scrollTop; // Potential reflow
3. Batch Multiple Mutations
// ✅ یک write برای همه
window.setStylesSafe(element, {
width: '100px',
height: '100px',
opacity: '1'
});
// ❌ سه write جداگانه
element.style.width = '100px';
element.style.height = '100px';
element.style.opacity = '1';
4. استفاده از will-change برای Animations
.animated-element {
will-change: transform, opacity;
}
مزایا:
- مرورگر میتواند از قبل آماده شود
- GPU acceleration
- Smoother animations
5. استفاده از IntersectionObserver
// ✅ GOOD: IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Element visible!
}
});
});
observer.observe(element);
// ❌ BAD: Manual scroll checking
$(window).on('scroll', () => {
const rect = element.getBoundingClientRect(); // Reflow!
if (rect.top < window.innerHeight) {
// Element visible
}
});
🔍 How to Test
1. Chrome DevTools Performance
- باز کردن Chrome DevTools (F12)
- رفتن به تب Performance
- کلیک Record و scroll کردن صفحه
- Stop recording
- جستجو برای “Recalculate Style” و “Layout”
نتیجه خوب: Layout events کمتر از 50ms
2. PageSpeed Insights
https://pagespeed.web.dev/analysis?url=https://staging.xpay.co/coin/uvoucher/
هدف: صفر warning برای Forced Reflow
3. Console Log (Debug Mode)
اگر WP_DEBUG فعال باشد:
console.log('[Reflow Fix] Initialized');
📚 منابع
- Web.dev - Avoid Large, Complex Layouts
- MDN - Performance Best Practices
- Paul Irish - What Forces Layout/Reflow
- FastDOM Library
✅ Checklist Integration
برای اضافه کردن این بهینهسازی به پروژههای جدید:
- کپی
reflow-fix.jsبه/assets/js/ - اضافه کردن به
Assets.phpبا priority بالا (load in<head>) - جایگزینی
$(window).scrollTop()باgetScrollPositionSafe() - Wrap کردن DOM reads/writes با
DOMQueue - تست با PageSpeed Insights
- بررسی Performance tab در Chrome DevTools
نگهداری شده توسط: تیم XPay
آخرین بروزرسانی: دی 1403