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

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

دعم المتصفح

  • Chrome: 59.
  • الحافة: 18.
  • Firefox: 99
  • Safari: الإصدار 15.4.

المصدر

ملخّص

المشكلة

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

وتعتمد مدة التشغيل على الجهاز والشروط. وعادة ما تكون حوالي 50 ملّي ثانية. أما على الهاتف الجوّال، فتبلغ مدة الفيديو 250 ملي ثانية تقريبًا. وفي الحالات القصوى (بطء الأجهزة أو حالة وحدة المعالجة المركزية (CPU) في حالة طوارئ)، يمكن أن يتجاوز حجمها 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
});

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