تسريع مشغّل الخدمات باستخدام عمليات التحميل المُسبقة للتنقّل

تتيح لك ميزة "التحميل المُسبَق للتنقّل" التغلّب على وقت بدء تشغيل عامل الخدمة من خلال تقديم الطلبات بالتوازي.

Browser Support

  • Chrome: 59.
  • Edge: 18.
  • Firefox: 99.
  • Safari: 15.4.

Source

ملخّص

المشكلة

عند الانتقال إلى موقع إلكتروني يستخدم عامل خدمة للتعامل مع أحداث الجلب، يطلب المتصفّح من عامل الخدمة الحصول على ردّ. يتضمّن ذلك تشغيل عامل الخدمة (إذا لم يكن قيد التشغيل)، وإرسال حدث الجلب.

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

لا يشكّل وقت بدء التشغيل مشكلة إذا كنت تردّ من ذاكرة التخزين المؤقت، لأنّ فائدة تخطّي الشبكة أكبر من تأخير بدء التشغيل. ولكن إذا كنت تردّ باستخدام الشبكة…

SW boot
طلب التنقّل

يتأخر طلب الشبكة بسبب بدء تشغيل عامل الخدمة.

نواصل العمل على تقليل وقت بدء التشغيل من خلال استخدام التخزين المؤقت للتعليمات البرمجية في V8، وتخطّي عاملي الخدمة الذين لا يتضمّنون حدث جلب، وتشغيل عاملي الخدمة بشكل تخميني، وغير ذلك من التحسينات. ومع ذلك، سيكون وقت بدء التشغيل دائمًا أكبر من صفر.

لفتت شركة Facebook انتباهنا إلى تأثير هذه المشكلة، وطلبت إيجاد طريقة لتنفيذ طلبات التنقّل بالتوازي:

SW boot
طلب التنقّل

التحميل المُسبَق للتنقّل هو الحلّ

ميزة "التحميل المُسبَق للانتقال" هي ميزة تتيح لك أن تقول: "عندما يرسل المستخدم طلب انتقال GET، ابدأ طلب الشبكة أثناء بدء تشغيل عامل الخدمة".

لا يزال تأخير بدء التشغيل موجودًا، ولكنّه لا يحظر طلب الشبكة، وبالتالي يحصل المستخدم على المحتوى بشكل أسرع.

في ما يلي فيديو يوضّح كيفية عمل ذلك، حيث يتم منح عامل الخدمة تأخيرًا متعمدًا في بدء التشغيل يبلغ 500 ملي ثانية باستخدام حلقة while:

إليك العرض التوضيحي نفسه. للاستفادة من مزايا التحميل المُسبَق للتنقل، عليك استخدام متصفّح يتيح هذه الميزة.

تفعيل ميزة "التحميل المُسبَق للتنقّل"

addEventListener('activate', event => {
  event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
      // Enable navigation preloads!
      await self.registration.navigationPreload.enable();
    }
  }());
});

يمكنك الاتصال بالرقم navigationPreload.enable() متى أردت، أو إيقاف هذه الميزة باستخدام navigationPreload.disable(). ومع ذلك، بما أنّ الحدث fetch يحتاج إلى استخدامه، من الأفضل تفعيله وإيقافه في الحدث activate الخاص ببرنامج عامل الخدمة.

استخدام الردّ المُحمَّل مسبقًا

سيُجري المتصفّح الآن عمليات تحميل مُسبَق لعمليات التنقّل، ولكن سيظل عليك استخدام الاستجابة:

addEventListener('fetch', event => {
  event.respondWith(async function() {
    // Respond from the cache if we can
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    // Else, use the preloaded response, if it's there
    const response = await event.preloadResponse;
    if (response) return response;

    // Else try the network.
    return fetch(event.request);
  }());
});

event.preloadResponse هو وعد يتم تنفيذه بردّ في الحالات التالية:

  • تم تفعيل ميزة "التحميل المُسبق للتنقّل".
  • الطلب هو طلب GET.
  • الطلب هو طلب تنقّل (تنشئه المتصفّحات عند تحميل الصفحات، بما في ذلك إطارات iframe).

بخلاف ذلك، سيظل event.preloadResponse متوفّرًا، ولكن سيتم حلّه باستخدام undefined.

إذا كانت صفحتك بحاجة إلى بيانات من الشبكة، أسرع طريقة هي طلبها في عامل الخدمة وإنشاء ردّ واحد متدفّق يحتوي على أجزاء من ذاكرة التخزين المؤقت وأجزاء من الشبكة.

لنفترض أنّنا نريد عرض مقالة:

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  const includeURL = new URL(url);
  includeURL.pathname += 'include';

  if (isArticleURL(url)) {
    event.respondWith(async function() {
      // We're going to build a single request from multiple parts.
      const parts = [
        // The top of the page.
        caches.match('/article-top.include'),
        // The primary content
        fetch(includeURL)
          // A fallback if the network fails.
          .catch(() => caches.match('/article-offline.include')),
        // The bottom of the page
        caches.match('/article-bottom.include')
      ];

      // Merge them all together.
      const {done, response} = await mergeResponses(parts);

      // Wait until the stream is complete.
      event.waitUntil(done);

      // Return the merged response.
      return response;
    }());
  }
});

في المثال أعلاه، mergeResponses هي دالة صغيرة تدمج عمليات بث كل طلب. وهذا يعني أنّه يمكننا عرض العنوان المخزّن مؤقتًا أثناء بث محتوى الشبكة.

هذه الطريقة أسرع من نموذج "غلاف التطبيق" لأنّ طلب الشبكة يتم إجراؤه مع طلب الصفحة، ويمكن بث المحتوى بدون اختراقات كبيرة.

ومع ذلك، سيتأخر طلب includeURL بسبب وقت بدء تشغيل مشغّل الخدمات. يمكننا استخدام ميزة "التحميل المُسبَق للتنقّل" لإصلاح هذه المشكلة أيضًا، ولكن في هذه الحالة لا نريد التحميل المُسبَق للصفحة الكاملة، بل نريد التحميل المُسبَق لتضمين.

لإتاحة ذلك، يتم إرسال عنوان مع كل طلب تحميل مُسبَق:

Service-Worker-Navigation-Preload: true

يمكن للخادم استخدام ذلك لإرسال محتوى مختلف لطلبات التحميل المُسبَق للتنقّل عن المحتوى الذي يتم إرساله لطلب تنقّل عادي. ما عليك سوى تذكُّر إضافة عنوان Vary: Service-Worker-Navigation-Preload، لكي تعرف ذاكرات التخزين المؤقت أنّ ردودك مختلفة.

يمكننا الآن استخدام طلب التحميل المُسبَق:

// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
  // Else do a normal fetch
  .then(r => r || fetch(includeURL))
  // A fallback if the network fails.
  .catch(() => caches.match('/article-offline.include'));

const parts = [
  caches.match('/article-top.include'),
  networkContent,
  caches.match('/article-bottom')
];

تغيير العنوان

تكون القيمة التلقائية للعنوان Service-Worker-Navigation-Preload هي true، ولكن يمكنك ضبطها على أي قيمة تريدها:

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
  console.log('Done!');
});

يمكنك، على سبيل المثال، ضبطه على معرّف آخر مشاركة تم تخزينها مؤقتًا على جهازك، وبالتالي لن يعرض الخادم سوى البيانات الأحدث.

الحصول على الولاية

يمكنك البحث عن حالة التحميل المُسبَق للتنقّل باستخدام getState:

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.getState();
}).then(state => {
  console.log(state.enabled); // boolean
  console.log(state.headerValue); // string
});

نشكر "مات فالكنهاغن" و"تسويوشي هورو" على عملهما على هذه الميزة ومساعدتهما في كتابة هذه المقالة. ونودّ أن نشكر جميع المشاركين في جهود التوحيد.