العودة إلى المدونة

انتقال حالة الظهور: قياس مشاهدات الصفحات بلا إشارات beacon موثوقة

تختلف سلوكيات pagehide و visibilitychange و Beacon API من متصفح إلى آخر. إليك كيفية تسجيل نهاية الجلسة بشكل موثوق دون كوكيز أو معرّفات دائمة.

تسجيل أن صفحة ما قد شوهدت أمر سهل. أما تسجيل اللحظة التي غادر فيها المستخدم فعلًا — بموثوقية، عبر تبديل التبويبات والانتقال إلى الخلفية والإغلاق المفاجئ — فهو من أصعب مسائل تحليلات الويب. وأهميته أن مقاييس التفاعل (مدة البقاء، الارتداد، عمق التمرير) تعتمد على التقاط إشارة نظيفة لنهاية الجلسة. إن أخطأت فيها، فإما أن تفقد بيانات أو أن تُضخّمها.

يحلّ Monoid هذه المسألة بلا كوكيز ولا localStorage ولا أي معرّف دائم. وهذا القيد يُبسّط الأمور في الواقع: فنحن لا نحتاج أبدًا إلى مطابقة جلسة عبر عمليات تحميل الصفحة، وكل ما يهمّنا هو إرسال إشارة ختامية واحدة دقيقة لكل دورة حياة صفحة. وفيما يلي كيف تتصرف أوليّات المتصفح، وأين تكمن المزالق.

لماذا unload هو الحدث الخطأ

كان النهج التاريخي هو الإنصات إلى unload أو beforeunload وإطلاق طلب XHR متزامن. وهذا اليوم ضار فعليًا. فحدث unload غير موثوق على الأجهزة المحمولة: حين ينقل المستخدم التبويب إلى الخلفية ويستعيد نظام التشغيل موارده، كثيرًا ما لا يُطلق unload إطلاقًا. والأسوأ أن تسجيل معالج لـ unload يُخرج الصفحة من ذاكرة التنقل للخلف/للأمام (bfcache) في عدة متصفحات، ما يضرّ بأداء التنقل.

إرشادات web.dev حول Page Lifecycle API صريحة: عامل انتقال visibilitychange إلى hidden بوصفه آخر حدث موثوق ستراه. ولا تعتمد على إطلاق unload أو pagehide على الأجهزة المحمولة.

اقتران visibilitychange مع pagehide

النمط المتين هو الإنصات إلى visibilitychange والتحقق من document.visibilityState === 'hidden'، مع pagehide كإشارة ثانوية. وكلما انتقلت الصفحة إلى حالة الإخفاء، تُفرغ ما لديك لإرساله. وتوثيق MDN لـ Page Visibility API يؤكد أن visibilitychange يُطلق عند نقل التبويب إلى الخلفية أو تصغير المتصفح — وهو أقرب بديل لعبارة «توقّف المستخدم عن النظر».

والتعقيد هنا أن الانتقال إلى الإخفاء قد يُطلق مرات عدة في الجلسة الواحدة (مغادرة التبويب، ثم العودة، ثم المغادرة ثانيةً). لذا عليك أن تجعل عملية الإفراغ عديمة الأثر عند التكرار (idempotent)، أو أن تُطبّق عليها debounce، أو أن ترسل الفرق منذ آخر إفراغ فقط. ولأن Monoid لا يحتفظ بأي حالة خاصة بالمستخدم، فإننا نرسل حمولة واحدة مكتفية بذاتها ونترك للحافة إزالة التكرار في نطاق الطلب.

إرسال البيانات من صفحة تحتضر: navigator.sendBeacon

لا يمكنك إطلاق fetch غير متزامن أثناء visibilitychange وتتوقع اكتماله — فقد يهدم المتصفح الصفحة قبل ذلك. ولهذا الغرض بالذات وُجدت مواصفة Beacon API. فالدالة navigator.sendBeacon(url, data) تضع في الطابور طلب POST صغيرًا يضمن المتصفح محاولة إرساله حتى بعد زوال المستند، دون حجب الخيط الرئيسي أو تأخير التنقل.

ولـ Beacon حدود يجدر احترامها:

  • حجم الحمولة. تسمح المواصفة لوكيل المستخدم برفض الإشارات التي تتجاوز حدًا يحدده التنفيذ (64 كيلوبايت عادةً). أبقِ الحمولات ضئيلة — حمولات Monoid بضع مئات من البايتات.
  • الطريقة. الإشارات دائمًا POST. وإن كانت نقطة النهاية على الحافة تتوقع GET، فعدّلها.
  • لا استجابة. لا يمكنك قراءة أي شيء عائد. أطلِق وانسَ.

والبديل الحديث هو fetch(url, { keepalive: true })، الذي يُعرّفه معيار Fetch بأنه يسمح للطلب بأن يعيش بعد المستند. ويمنحك keepalive ترويسات وطرائق تفتقر إليها Beacon، لكنه يشترك معها في السقف نفسه للحجم. استخدم Beacon أولًا، ثم عُد إلى fetch مع keepalive.

تنفيذ أدنى بلا كوكيز

let sent = false;
function flush() {
  if (sent) return;
  sent = true;
  const payload = JSON.stringify({
    path: location.pathname,
    // بلا معرّفات، بلا كوكيز، بلا بصمة
    visibleMs: Math.round(performance.now())
  });
  const ok = navigator.sendBeacon('/collect', payload);
  if (!ok) {
    fetch('/collect', { method: 'POST', body: payload, keepalive: true });
  }
}

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('pagehide', flush);

لاحظ ما هو غائب: لا مستمع لـ unload، فتبقى أهلية bfcache محفوظة. ولا معرّف مخزَّن بين عمليات التحميل. وقيمة visibleMs تستعمل performance.now()، وهي ساعة رتيبة لا تحتاج طابعًا زمنيًا من ساعة الحائط ولا حالة دائمة.

لماذا يناسب هذا التحليلات التي تضع الخصوصية أولًا

لأن Monoid لا يخيط الجلسات معًا أبدًا، فكل إشارة ملاحظة كاملة قابلة للتخلص منها. ولا يوجد معرّف عابر للصفحات يجب حمايته، فسقوط إشارة يكلّفك نقطة بيانات واحدة لا ملفًا شخصيًا مشوَّهًا. ويمنحك نمط visibilitychange مع Beacon قياسًا دقيقًا لمدة البقاء مع إبقاء الصفحة سريعة وصديقة لـ bfcache — وهو ما يُحسّن بذاته مؤشرات Core Web Vitals التي قد تحاول قياسها أصلًا.

قِس المغادرة، لا الشخص. فالمتصفح يمنحك بالفعل كل ما تحتاج إليه.

Sources

Comments

Loading comments…