توسعه فرانت‌اند

Intersection Observer و Lazy Loading حرفه‌ای

تشخیص تقاطع با viewport، rootMargin و threshold، پاک‌سازی observer و نمونه کامل بارگذاری ماژول با دکمه جایگزین و مدیریت خطا.

1404/11/11 3 دقیقه مطالعه 2,570 بازدید

اگر یک بخش سنگین پایین صفحه است، می‌توانید آماده‌سازی آن را تا نزدیک شدن کاربر به همان بخش عقب بیندازید. Intersection Observer تغییر تقاطع یک عنصر با viewport یا container را گزارش می‌کند. این API برای شروع کار در یک محدوده تقریبی مناسب است؛ اندازه‌گیری دقیق هر pixel حرکت یا تشخیص قطعی خوانده شدن محتوا وظیفه دیگری است.

Lazy Loading باید با اهمیت محتوا هماهنگ باشد. اطلاعات ضروری تصمیم، navigation و بخش اصلی صفحه نباید فقط به امید رسیدن یک callback در دسترس قرار گیرند. برای محتوای اختیاری، تأخیر می‌تواند هزینه اولیه را محدود کند، ولی state انتظار، شکست و مسیر دستی لازم‌اند. «بدون scroll event» به تنهایی طراحی کامل یا سریع‌تر بودن تمام صفحه را ثابت نمی‌کند.

تقاطع را با دیده شدن واقعی یکی نگیرید

راهنمای Intersection Observer در MDN این API را روش ناهمزمان مشاهده تغییر تقاطع معرفی می‌کند. target ممکن است با ناحیه مرجع تقاطع داشته باشد اما کاربر به آن توجه نکرده باشد. overlay، زمان حضور و وضعیت تب می‌توانند مهم باشند. عدد intersectionRatio به تنهایی گزارش «مطالعه محتوا» نیست.

برای نمونه فرضی مقاله بلند، نزدیک شدن به بخش جزئیات می‌تواند شروع دریافت ماژول اختیاری را توجیه کند. این مثال رفتار مشتری واقعی یا اندازه‌گیری عملکرد نیست. callback فقط می‌گوید شرط هندسی انتخاب‌شده رخ داده است. اگر هدف Analytics است، تعریف رویداد باید دقیق‌تر باشد و محدودیت آن در گزارش حفظ شود.

گزینه‌های visibility پیشرفته محدودیت و هزینه خود را دارند. پشتیبانی مرورگرهای هدف را از مستندات جاری بررسی کنید و API پایه را با قابلیت تشخیص پوشیده شدن کامل یکسان ندانید. برای بسیاری از Lazy Loadingها، تقاطع تقریبی کافی است. افزودن محاسبه گران فقط برای گرفتن نام «حرفه‌ای»، بدون نیاز مشخص فایده‌ای ندارد.

root و rootMargin محدوده را تعیین می‌کنند

root می‌تواند viewport پیش‌فرض یا ancestor مناسب باشد. اگر target داخل یک پنل scroll مستقل قرار دارد، انتخاب root باید با همان رفتار هماهنگ باشد. استفاده از viewport وقتی کاربر فقط داخل پنل حرکت می‌کند ممکن است برداشت مورد انتظار را ندهد. ساختار scroll و clipping را پیش از تعیین options بررسی کنید.

rootMargin مرز محاسبه تقاطع را بزرگ یا کوچک می‌کند. مقدار مثبت می‌تواند کار را پیش از ورود واقعی target به قاب شروع کند. این برای آماده‌سازی نزدیک به زمان نیاز مفید است، اما انتخاب margin خیلی بزرگ ممکن است بیشتر بخش‌ها را از ابتدا بارگیری کند. مقدار باید به وزن کار و رفتار استفاده مربوط باشد.

rootMargin اندازه‌گیری زمان باقی‌مانده نیست. فاصله ۲۰۰ pixel برای همه سرعت‌های scroll زمان یکسانی نمی‌سازد. آن را وعده آماده شدن پیش از مشاهده ننامید. اگر آماده‌سازی دیر شد، وضعیت انتظار لازم است. مسیر نمایش باید با نتیجه واقعی ماژول یا request هماهنگ باشد، نه با فرض اینکه ورود به margin همیشه فرصت کافی می‌دهد.

threshold و isIntersecting را درست بخوانید

threshold مقدار یا مجموعه نسبت‌هایی است که عبور از آن‌ها می‌تواند callback بسازد. تعداد بیشتر thresholdها اعلان‌های بیشتری ایجاد می‌کند و برای هر نیاز لازم نیست. اگر فقط می‌خواهید اولین نزدیکی را تشخیص دهید، مجموعه‌ای از صد نسبت فایده روشنی ندارد. target بسیار بلند نیز ممکن است هرگز به نسبت بزرگ مورد انتظار شما نرسد.

isIntersecting وضعیت تقاطع entry را بیان می‌کند. اولین observe هم می‌تواند گزارشی درباره وضعیت اولیه بدهد؛ callback را صرفاً به معنی «کاربر تازه scroll کرده» تفسیر نکنید. یک callback می‌تواند چند entry داشته باشد و هر کدام target و زمان خودش را دارد. همه را بدون بررسی، به یک عنصر ثابت نسبت ندهید.

برای کاری که یک‌بار لازم است، پس از موفقیت unobserve کنید. اگر هدف چندباره است، state ورود و خروج را جدا نگه دارید. دریافت duplicate callback نباید چند request یکسان آغاز کند. پرچم in-flight یا Promise مشترک می‌تواند هماهنگی ایجاد کند، ولی پس از خطا باید مسیر retry معلوم باشد و state معطل برای همیشه باقی نماند.

نمونه کامل بارگیری ماژول متنی اختیاری

دو فایل زیر را در یک پوشه با نام index.html و details.mjs ذخیره کنید. نمونه تصویر و dependency ندارد. برای import ماژول، فایل‌ها را از سرور محلی باز کنید؛ اگر Python دارید، در همان پوشه python -m http.server 8000 اجرا و localhost:8000 را باز کنید. اجرای مستقیم file URL ممکن است محدودیت module داشته باشد.

<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  <title>بارگیری ماژول نزدیک بخش اختیاری</title>
  <style>
    body { margin:0; color:#17212b; background:#f4f6f9; font:1rem/1.9 system-ui,sans-serif; }
    main { max-inline-size:42rem; margin-inline:auto; padding:2rem 1rem; }
    .intro { min-block-size:110vh; }
    section { padding:1rem; background:#fff; border:1px solid #64748b; }
    button { font:inherit; padding:.4rem .8rem; color:#fff; background:#164ca4; border:0; }
    :focus-visible { outline:3px solid #164ca4; outline-offset:4px; }
    [hidden] { display:none; }
  </style>
</head>
<body>
  <main>
    <div class="intro"><h1>محتوای اصلی از ابتدا حاضر است</h1><p>برای نزدیک شدن به بخش اختیاری، صفحه را به پایین ببرید. مثال هیچ تصویر یا سرویس بیرونی ندارد.</p></div>
    <section id="details" aria-labelledby="details-title" aria-busy="false">
      <h2 id="details-title">جزئیات اختیاری</h2>
      <p id="content">این توضیح از ابتدا موجود است؛ ماژول فقط متن تکمیلی اضافه می‌کند.</p>
      <button id="load" type="button" hidden>بارگیری جزئیات</button>
      <noscript><p>برای دریافت ماژول آموزشی، JavaScript لازم است.</p></noscript>
    </section>
    <p id="status" role="status"></p>
  </main>
  <script type="module">
    const target = document.getElementById('details');
    const button = document.getElementById('load');
    const status = document.getElementById('status');
    let observer;
    let pending = null;
    let loaded = false;
    async function load() {
      if (loaded) return;
      if (pending) return pending;
      target.setAttribute('aria-busy','true');
      button.disabled = true;
      status.textContent = 'ماژول اختیاری در حال دریافت است.';
      pending = (async () => {
        try {
          const module = await import('./details.mjs');
          document.getElementById('content').textContent = module.getDetails();
          loaded = true;
          observer?.unobserve(target);
          button.textContent = 'جزئیات دریافت شد';
          status.textContent = 'متن تکمیلی دریافت شد.';
        } catch {
          observer?.unobserve(target);
          button.textContent = 'تلاش دوباره برای جزئیات';
          status.textContent = 'دریافت ماژول ناموفق بود؛ مسیر فایل و سرور محلی را بررسی کنید.';
        } finally {
          target.setAttribute('aria-busy','false');
          button.disabled = loaded;
          pending = null;
        }
      })();
      return pending;
    }
    button.hidden = false;
    button.addEventListener('click',load);
    if ('IntersectionObserver' in window) {
      observer = new IntersectionObserver(entries => {
        for (const entry of entries) if (entry.isIntersecting) void load();
      }, { root:null, rootMargin:'150px 0px', threshold:0 });
      observer.observe(target);
      window.addEventListener('pagehide',() => observer.disconnect(),{ once:true });
    }
  </script>
</body>
</html>
export function getDetails() {
  return 'این متن از ماژول محلی آمده است. بارگیری هنگام نزدیک شدن به بخش یا انتخاب دکمه آغاز می‌شود؛ تقاطع به معنی مطالعه قطعی کاربر نیست.';
}

بخش پایین صفحه با فاصله واقعی از متن آغازین قرار دارد تا نزدیک شدن به target قابل فهم باشد. محتوای اصلی از ابتدا در HTML است و فقط جزئیات آموزشی اختیاری از ماژول می‌آید. دکمه دستی همیشه مسیر شروع را ارائه می‌کند. در نبود Intersection Observer، همان دکمه کار می‌کند؛ در نبود JavaScript، توضیح ثابت محدوده نمونه را بیان می‌کند.

load Promise درخواست‌های هم‌زمان از callback و دکمه را به یک مسیر محدود می‌کند. پس از موفقیت، target دیگر مشاهده نمی‌شود. در خطا، پیام و راه تلاش دوباره باقی می‌مانند و automatic retry بی‌پایان نداریم. import موفق نیز با API ماژول بررسی می‌شود؛ نام export باید با قرارداد مصرف‌کننده هماهنگ باشد.

برای تصویر، ابتدا ابزار بومی را بررسی کنید

loading در عنصر img مسیر بومی Lazy Loading را توضیح می‌دهد. برای بسیاری از تصویرهای خارج از بخش اولیه، نیازی به observer اختصاصی نیست. ابعاد و alt باید مستقل از زمان دریافت درست باشند. تصویر مهم بخش اول یا تصویر مؤثر بر تجربه اولیه را بدون بررسی نیاز، lazy نکنید.

بارگیری منبع با نمایش محتوا فرق دارد. حتی پس از دریافت، decode یا layout می‌تواند اثر داشته باشد. رزرو فضای مناسب از جابه‌جایی ناگهانی کمک می‌گیرد، ولی تضمین حذف تمام تغییرهای صفحه نیست. این مقاله عمداً هیچ فایل تصویری برای مثال لازم ندارد؛ تصمیم درباره دارایی واقعی باید با ابعاد و نقش همان دارایی گرفته شود.

برای component جاوااسکریپتی، dynamic import هزینه را به مرحله دیگر منتقل می‌کند. اگر کاربر تقریباً همیشه از component استفاده می‌کند، تأخیر ممکن است فقط انتظار بعدی بسازد. وزن ماژول و فراوانی استفاده را بررسی کنید. Lazy Loading هدف مستقل محصول نیست؛ ابزاری برای تقسیم کار بر اساس زمان نیاز است.

Infinite Scroll به قرارداد فهرست نیاز دارد

sentinel نزدیک پایان فهرست می‌تواند شروع دریافت صفحه بعد را نشان دهد، اما pagination، ترتیب و حالت پایان هنوز لازم‌اند. اگر یک درخواست در جریان است، callbackهای بعدی نباید صفحه‌ها را چندبار دریافت کنند. پایان داده را صریح نشان دهید و observer را متوقف کنید. خطا باید دکمه retry داشته باشد تا کاربر مجبور به حرکت تصادفی برای تحریک دوباره callback نشود.

حفظ موقعیت هنگام بازگشت از جزئیات و رسیدن به Footer برای فهرست اهمیت دارند. اضافه شدن دائمی محتوا ممکن است انتهای صفحه را دور کند. گزینه بارگذاری بیشتر گاهی اختیار روشن‌تری می‌دهد. مقاله سایت خبری برای آرشیو و فهرست این تفاوت را بررسی می‌کند. انتخاب infinite scroll را از نوع کار کاربر جدا نکنید.

محتوای تازه نباید Focus را خودکار از کنترل فعلی بگیرد. شمار نتیجه و وضعیت می‌توانند با پیام مناسب معرفی شوند. تمام فهرست را live region نکنید اگر خواندن دوباره متن را ایجاد می‌کند. Loading States برای انتظار و دسترسی‌پذیری برای ترتیب کار ادامه‌های مرتبط‌اند.

callback را محدود و cleanup را روشن نگه دارید

Observer تشخیص تقاطع را به مرورگر می‌سپارد، ولی callback شما همچنان می‌تواند کار سنگین ایجاد کند. محاسبه بزرگ، تغییرهای متعدد DOM یا requestهای زیاد در همان callback را بی‌نیاز از هزینه فرض نکنید. فقط تصمیم آغاز کار لازم را بگیرید و state مربوط را مدیریت کنید. برای animation پیوسته وابسته به scroll، نیاز ممکن است با callback تقاطع یکسان نباشد.

در componentهای دارای lifecycle، target حذف‌شده و observer باقی‌مانده را پاک کنید. disconnect برای پایان استفاده از observer و unobserve برای target مشخص کاربرد دارد. اگر برنامه target تازه می‌سازد، مشاهده آن هم باید صریح باشد. Promise و async/await برای لغو و نتیجه دیررس ادامه مرتبط است. مستند باید بگوید چه چیزی آغاز، متوقف و دوباره تلاش می‌شود.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!