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

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

توافق المتصفّح

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

المصدر

ملخّص

المشكلة

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

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

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

تشغيل SW
طلب التنقّل

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

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

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

تشغيل SW
طلب التنقّل

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

ميزة "التحميل المُسبَق للتنقّل" هي ميزة تتيح لك قول "عندما يُجري المستخدم طلب تنقّل 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
});

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