← بازگشت به صفحه اصلی

تاریخ: دی 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);
  });
});

مزایا:

window.getScrollPositionSafe()

دریافت scroll position بدون forced reflow:

const { top, left } = window.getScrollPositionSafe();
// Cache می‌شود برای همان frame - هزاران بار بخوانید بدون reflow!

ویژگی‌ها:

window.getDimensionsSafe(element)

دریافت تمام dimensions یک element:

const dims = window.getDimensionsSafe(element);
// dims = {
//   width, height,
//   clientWidth, clientHeight,
//   scrollWidth, scrollHeight
// }

مزایا:

element.getBoundingClientRectCached()

Override بهینه شده برای getBoundingClientRect():

const rect = element.getBoundingClientRectCached();
// همان rect اما cached!

window.isElementVisibleSafe(element)

چک کردن visibility بدون getBoundingClientRect:

if (window.isElementVisibleSafe(element)) {
  // Element در viewport هست
}

روش کار:

✅ 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>?


📈 نتایج

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;
}

مزایا:

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

  1. باز کردن Chrome DevTools (F12)
  2. رفتن به تب Performance
  3. کلیک Record و scroll کردن صفحه
  4. Stop recording
  5. جستجو برای “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');

📚 منابع


✅ Checklist Integration

برای اضافه کردن این بهینه‌سازی به پروژه‌های جدید:


نگهداری شده توسط: تیم XPay
آخرین بروزرسانی: دی 1403