اذهب إلى المحتوى

توفر الواجهة البرمجية لعامل الخدمة Service Worker أدوات متعددةً وواسعة الاستخدامات، تتميز بمرونتها وتقديمها لأداء أفضل، إذا لم تستخدم عامل الخدمة سابقًا -ولا يمكن لومك على ذلك لأنه لم يلق تبنيًا واسعًا حتى عام 2020- فإليك طريقة عمله:

  1. عند أول زيارة إلى الموقع سيسجل المتصفح وكيلًا من طرف العميل، يعمل على كمية صغيرة من جافاسكربت تعمل في خيط thread خاص بها، مثل عامل الويب.
  2. بعد تسجيل عامل الخدمة يمكنك مقاطعة الطلبات الصادرة، وتحديد كيفية الرد عليها في حدث عامل الخدمة ()fetch.

ما ستفعله للطلبات التي تُقاطعها يعود لك ويعتمد على موقعك الإلكتروني، يمكنك إعادة كتابة الطلبات، والتخزين المؤقت المسبق للملفات الثابتة أثناء التثبيت، وتقديم ميزة العمل بدون اتصال بالإنترنت، وتوصيل حمولات أصغر من HTML لتقديم أداء أفضل لزوّار الموقع المتكررين، وهو ما سنركز عليه في مقالنا.

تجاوز الاتصال الضعيف بالشبكة

سنشرح حالةً عمليةً لتوضيح الفكرة، ففي ولاية ويسكونسون الأميركية قدمت شركة ويكلي تيمبر Weekly Timber خدمات قطع الأشجار ونقلها، وبالطبع ستلعب سرعة أداء الموقع دورًا كبيرًا في عملهم، فمكان عمل الشركة هو مقاطعة واشيرا Waushara في الولاية، وليست جودة الاتصال بالشبكة ووثوقيتها هناك جيدةً، مثل العديد من المناطق الريفية في الولايات المتحدة الأميريكية.

fig-1.png

توضح الخارطة تغطية الشبكة اللاسلكية في مقاطعة واشيرا في ولاية ويسكونسون، حيث تعني المناطق ذات اللون الأسمر سرعات منخفضةً تتراوح بين 3 إلى 9.99 ميجا بت في الثانية، بينما تعني المناطق ذات اللون الأحمر سرعات أبطأ من ذلك، والمناطق ذات اللونين الأزرق الداكن والباهت السرعات الأسرع من ذلك.

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

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

اعتمد أول عامل خدمة أضيف إلى الموقع -سنشير إليه لاحقًا بعامل الخدمة "الأساسي standard"- على ثلاث استراتيجيات للتخزين المؤقت:

  1. التخزين المؤقت المسبق لملفات جافاسكريبت والتنسيقات الموروثة CSS لجميع الصفحات عند تثبيت عامل الخدمة بعد إطلاق حدث التحميل load للنافذة.
  2. تخديم الملفات الثابتة static من مخزن التخزين المؤقت CacheStorage عند توافرها، فإذا لم تتوافر فستُجلب من الشبكة، ثم تخزَّن تخزينًا مؤقتًا لتُخدم عند الزيارات اللاحقة للموقع.
  3. تخديم ملفات HTML من الشبكة أولًا، ثم تخزينها في مخزن التخزين المؤقت CacheStorage، وإذا لم يتوافر الاتصال بالشبكة في الزيارات اللاحقة للموقع فسيُخدم ملف HTML المطلوب من التخزين المؤقت.

الاستراتيجيات السابقة ليست مميزةً أو جديدةً، وهي تقدم الفائدتين التاليتين:

  • إمكانية العمل بدون الاتصال بالشبكة، وهو أمر مفيد في حالات الاتصال الضعيف بالشبكة.
  • رفع أداء تخديم الملفات الثابتة بشكل كبير.

أدى رفع الأداء هذا إلى تحسن بنسبة 42% و 48% لكل من المؤشرين أول طباعة للمحتوى First Contentful Paint، واختصارًا FCP، وأكبر طباعة للمحتوى Largest Contentful Paint، واختصارًا LCP، وهذه الأرقام مبنية على مراقبة المستخدم الحقيقية RUM. ما يعني أن تلك المكاسب ليست نظريةً فقط، بل هي تحسن حقيقي لأشخاص واقعيين.

fig-2.png

يوضح هذا المخطط مدة الطلب/الجواب لأدوات المطور في جوجل كروم، والطلب الموضح هو لملف ثابت من مخزن التخزين المؤقت CacheStorage، حيث استغرق عامل الخدمة 23 ميلي ثانية فقط لتحميل هذا الملف، بسبب عدم الحاجة للاتصال بالشبكة، وإمكانية تحميله من CacheStorage مباشرةً.

تحسن الأداء هو نتيجة تجاوز الاتصال بالشبكة كليًا للملفات الثابتة الموجودة مسبقًا في CacheStorage، خصوصًا ملفات التنسيق المعيقة للتصيير، يمكن تحقيق تحسن شبيه بالأداء السابق بالاعتماد على التخزين المؤقت لطلبات HTTP، وسنلاحظ التشابه من حيث الأداء السابق مع FCP و LCP دون الاعتماد على عامل الخدمة نهائيًا.

قد تتساءل عن الفرق إذًا بين CacheStorage والتخزين المؤقت لطلبات HTTP، يكمن الفرق في أن التخزين المؤقت لطلبات HTTP يحتاج -على الأقل في بعض الحالات- لإرسال طلب إلى الخادم للتحقق من حداثة الملف الموجود في التخزين المؤقت، ويمكن حل هذه المشكلة باستخدام القيمة immutable للترويسة Cache-Control، لكن ليس لها دعم واسع حاليًا، ويوجد حل آخر بتعيين قيمة عمرية كبيرة للملفات في max-age، لكن المزيج بين الواجهة البرمجية لعامل الخدمة وCacheStorage يوفر مرونةً أكبر.

نستنتج مما سبق أن أبسط تضمين لعامل الخدمة يمكن أن يقدم تحسينًا في الأداء، وربما أفضل مما توفره ترويسة Cache-Control، ويمكن لعامل الخدمة أيضًا توفير مزايا واحتمالات أكبر، وهذا ما سنشاهده في هذا المقال.

عامل خدمة أسرع وأفضل

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

توفر الواجهة البرمجية لعامل الخدمة مساحة ابتكار واسعة نسبيًا، تؤدي لأثر كبير على تجربة الويب، ويمكن لبعض الأمور، مثل التحميل المسبق للتنقل ومجرى القراءة ReadableStream، أن تحول عامل الخدمة من أمر جيد إلى سيء، يمكن باستخدام تلك المزايا توفير الإمكانيات التالية على الترتيب:

  • تقليص وقت استجابة عامل الخدمة عبر تمكين العمل على التوازي بين وقت إقلاع عامل الخدمة وإرسال طلبات التنقل.
  • التحكم في تدفق بيانات المحتوى القادم من CacheStorage والشبكة.

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

تحضير الأساسيات

تبدو فكرة تجميع أجزاء الترويسة والتذييل في الموقع مع المحتوى القادم من الشبكة شبيهةً بالتطبيقات أحادية الصفحة Single Page Application، واختصارًا SPA، فهي مثلها ستحتاج لتطبيق نموذج "صدفة التطبيق app shell" على موقعك، لكن بدلًا من موجّه من طرف العميل يحاول تجميع المحتوى في قطعة صغيرة واحدة من الترميز، يجب أن تتصور الموقع مثل ثلاث قطع منفصلة:

  • الترويسة
  • المحتوى
  • التذييل

سيبدو ذلك بالشكل التالي في موقع الشركة:

fig-3.png

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

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

سنبدأ أولًا بالتخزين المؤقت لقسمي الترويسة والتذييل عند تثبيت عامل الخدمة، تخدَّم هذه الأجزاء في موقع الشركة في المسارات partial-header/ وpartial-footer/:

self.addEventListener("install", event => {
  const cacheName = "اسم للتخزين المؤقت هنا";
  const precachedAssets = [
    "/partial-header",  // جزئية الترويسة
    "/partial-footer",  // جزئية التذييل
    // ملفات أخرى نريد تخزينها تخزينًا مؤقتًا
  ];

  event.waitUntil(caches.open(cacheName).then(cache => {
    return cache.addAll(precachedAssets);
  }).then(() => {
    return self.skipWaiting();
  }));
});

يجب أن نكون قادرين على جلب محتوى كل صفحة دون الترويسة والتذييل وكذلك معهما، وهذا ضروري لأن عامل الخدمة لن يتحكم بأول زيارة للموقع، لكن عندما يتولى عامل الخدمة يمكننا جلب جزئية المحتوى وتجميعها ضمن استجابة كاملة للصفحة مع الترويسة والتذييل من CacheStorage.

إذا كان موقعك ذا محتوى ثابت، فذلك يعني توليد العديد من جزئيات الترميز التي يمكنك إعادة كتابة طلباتها في حدث عامل الخدمة ()fetch، أما إذا احتوى موقعك واجهةً خلفيةً -كما حال موقع الشركة في مثالنا- فيمكنك استخدام ترويسة طلب HTTP لتحدد للخادم ما إذا كنت تريد صفحةً كاملةً أم أجزاء المحتوى فقط.

القسم الأصعب لدينا هو تجميع القطع معًا، وهو ما سنفعله الآن.

تجميع القطع مع بعضها

تعَد كتابة عامل خدمة بسيط تحديًا، لكن الأمور ستتعقد بسرعة عند محاولتنا تجميع عدة استجابات معًا، وأحد أسباب ذلك هو أننا سنحتاج لإعداد التحميل المسبق للتنقل لتجاوز الوقت اللازم لإقلاع عامل الخدمة.

تضمين التحميل المسبق للتنقل

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

يجب تفعيل التحميل المسبق للتنقل صراحةً، لن يؤخر عامل الخدمة بعد تفعيله طلبات التنقل خلال إقلاعه، ويمكن تفعيل التحميل المسبق للتنقل ضمن حدث عامل الخدمة activate كالتالي:

self.addEventListener("activate", event => {
  const cacheName = "اسم للتخزين المؤقت هنا";
  const preloadAvailable = "navigationPreload" in self.registration;

  event.waitUntil(caches.keys().then(keys => {
    return Promise.all([
      keys.filter(key => {
        return key !== cacheName;
      }).map(key => {
        return caches.delete(key);
      }),
      self.clients.claim(),
      preloadAvailable ? self.registration.navigationPreload.enable() : true
    ]);
  }));
});

يجب أن نتحقق من توفر الميزة بسبب عدم الدعم الواسع للتحميل المسبق للتنقل، وهو ما فعلناه في المثال السابق ،وخزّنا نتيجته في المتغير preloadAvailable.

بالإضافة لاحتياجنا استخدام ()Promise.all لجلب عدة عمليات غير متزامنة قبل أن تفعيل عامل الخدمة، من تلك العمليات تنظيف بيانات التخزين المؤقت القديمة، وانتظار كلٍ من()clients.claim -وهي التي تخبر عامل الخدمة بالتحكم حالًا بدلًا من انتظار عملية التنقل القادمة- وعملية تفعيل التحميل المسبق للتنقل.

استخدمنا المعامل الثلاثي عند تفعيل التحميل المسبق للتنقل في المتصفحات التي توفر دعمًا له، وذلك لتجنب رمي الاستثناءات في المتصفحات التي لا تدعم تلك الميزة، ونفعل التحميل المسبق للتنقل إذا كانت قيمة preloadAvailable هي true، أما إن لم تكن كذلك فنمرر قيمةً منطقية بوليانيةً لا تؤثر على قبول التابع ()Promise.all.

عند تفعيل التحميل المسبق للتنقل، داخل معالج الحدث ()fetch في عامل الخدمة لدينا، يجب أن نكتب شيفرةً تستفيد من جواب الطلب الذي سبق تحميله:

self.addEventListener("fetch", event => {
  const { request } = event;

  // تم اختصار شيفرة معالجة الملفات الثابتة للتوضيح
  // ...

  // التحقق فيما إذا كان الطلب لمستند
  if (request.mode === "navigate") {
    const networkContent = Promise.resolve(event.preloadResponse).then(response => {
      if (response) {
        addResponseToCache(request, response.clone());

        return response;
      }

      return fetch(request.url, {
        headers: {
          "X-Content-Mode": "partial"
        }
      }).then(response => {
        addResponseToCache(request, response.clone());

        return response;
      });
    }).catch(() => {
      return caches.match(request.url);
    });

    // سنضيف المزيد هنا...
  }
});

مع أن هذه ليست الشيفرة الكاملة للحدث ()fetch لعامل الخدمة، إلا أن فيها ما يحتاج الشرح:

  1. يُتاح الجواب المحمّل مسبقًا في المتغير event.preloadResponse، وستكون تلك القيمة undefined في المتصفحات التي لا تدعم التحميل المسبق للتنقل، لذا يجب تمرير event.preloadResponse للتابع ()Promise.resolve لتجنب مشاكل التوافقية تلك.
  2. بحسب ناتج الدالة then، إذا كان event.preloadResponse مدعومًا فسنستخدم الجواب المحمّل مسبقًا ونضيفه إلى CacheStorage عبر استدعاء الدالة المساعدة ()addResponseToCache، أما إن لم يكن مدعومًا فسنرسل طلبًا عبر الشبكة لجلب جزئية المحتوى عبر طلب ()fetch، بتعيين الترويسة المخصصة X-Content-Mode بالقيمة partial.
  3. إذا كان الاتصال بالشبكة غير متوفر حاليًا، فنعيد آخر نسخة من جزئية محتوى خُزنت في CacheStorage.
  4. نُعيد الجواب -بغض النظر عن مصدره- ونعينه قيمةً للمتغير networkContent الذي سنستخدمه لاحقًا.

عندما يفعَّل التحميل المسبق للتنقل، ستضاف الترويسة Service-Worker-Navigation-Preload بالقيمة true إلى طلبات التنقل، وسنتحقق في النظام الخلفي من هذه الترويسة لإرجاع جزئية المحتوى فقط بدلًا من ترميز الصفحة كاملًة.

لا يتوفر الدعم للتحميل المسبق للتنقل على جميع المتصفحات، لذا سنرسل ترويسةً مختلفةً في تلك الحالات، فسنستخدم في حالة موقع شركة ويكلي تيمبر ترويسةً مخصصةً بالاسم X-Content-Mode، وسنعين الثوابت التالية في الواجهة الخلفية للموقع:

<?php

// التحقق فيما إذا كان هذا طلب تنقل مسبق
define("NAVIGATION_PRELOAD", isset($_SERVER["HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD"]) && stristr($_SERVER["HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD"], "true") !== false);

// التحقق فيما إذا كان هذا طلب صريح لجزئية المحتوى
define("PARTIAL_MODE", isset($_SERVER["HTTP_X_CONTENT_MODE"]) && stristr($_SERVER["HTTP_X_CONTENT_MODE"], "partial") !== false);

// إذا كان أحد الحالتين صحيحًا فالطلب هو لجزئية المحتوى
define("USE_PARTIAL", NAVIGATION_PRELOAD === true || PARTIAL_MODE === true);

?>

يمكن بعد ذلك الاستعانة بقيمة الثابت USE_PARTIAL لتحديد نوعية الجواب:

<?php

if (USE_PARTIAL === false) {
  require_once("partial-header.php");
}

require_once("includes/home.php");

if (USE_PARTIAL === false) {
  require_once("partial-footer.php");
}

?>

إذا كنت تستخدم التخزين المؤقت لصفحات HTML، فيجب عليك تعيين قيمة للترويسة Vary لأجوبة طلبات HTML لتؤخَذ الترويسة Service-Worker-Navigation-Preload، وفي مثالنا أيضا الترويسة X-Content-Mode للتخزين المؤقت لطلبات HTML بالحسبان، وقد لا تحتاج لذلك إذا لم تستخدم التخزين المؤقت لـ HTML في مشروعك.

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

تدفق بيانات أجزاء المحتوى وتجميع ردود الطلبات

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

يجب الانتباه عند التعامل مع ReadableStream، فقد ينتهي الأمر بالتأثير على الأداء بدل تحسينه إذا أغفلنا بعض الخطوات الهامة، وسيكون التابع الذي يجمع الطلبات معًا كالتالي:

async function mergeResponses (responsePromises) {
  const readers = responsePromises.map(responsePromise => {
    return Promise.resolve(responsePromise).then(response => {
      return response.body.getReader();
    });
  });

  let doneResolve,
      doneReject;

  const done = new Promise((resolve, reject) => {
    doneResolve = resolve;
    doneReject = reject;
  });

  const readable = new ReadableStream({
    async pull (controller) {
      const reader = await readers[0];

      try {
        const { done, value } = await reader.read();

        if (done) {
          readers.shift();

          if (!readers[0]) {
            controller.close();
            doneResolve();

            return;
          }

          return this.pull(controller);
        }

        controller.enqueue(value);
      } catch (err) {
        doneReject(err);
        throw err;
      }
    },
    cancel () {
      doneResolve();
    }
  });

  const headers = new Headers();
  headers.append("Content-Type", "text/html");

  return {
    done,
    response: new Response(readable, {
      headers
    })
  };
}

أهم ما في التابع السابق:

  1. يقبل التابع ()mergeResponses الوسيط responsePromises، وهو مصفوفة تحوي كائنات من النوع Response، نحصل عليها إما من التحميل المسبق للتنقل أو ()fetch أو ()caches.match، وبفرض وجود اتصال بالشبكة ستحوي المصفوفة دومًا على ثلاث أجوبة، اثنان من ()caches.match وواحد من الشبكة.
  2. قبل أن نرسل الاستجابات داخل المصفوفة responsePromises، يجب أن نربط كل استجابة منها بقارئ واحد، يُستخدم لاحقًا في باني ()ReadableStream ليرسل محتوى كل استجابة منها.
  3. نُنشئ وعدًا Promise بالاسم done، نعين داخله تابعي الوعد ()resolve و ()reject للمتغيرات الخارجية doneResolve وdoneReject على التوالي، سيُستخدم المتغيران داخل ()ReadableStream للإشارة إلى نجاح تدفق البيانات من عدمه.
  4. تُنشأ النسخة الجديدة من ()ReadableStream بالاسم readable، وعندما تُرسل الاستجابات على شكل تدفق من CacheStorage والشبكة، سيضاف محتوى كل استجابة إلى readable.
  5. سيرسل التابع ()pull محتوى أول استجابة في المصفوفة على شكل تدفق، وإذا لم يُلغَ تدفق البيانات لسبب ما، فسيُتجاهل قارئ كل استجابة عبر استدعاء التابع ()shift في مصفوفة الاستجابات حالما ينتهي تدفق بيانات المحتوى كليًا، نكرر هذا الأمر إلى أن لا يبق أي قارئ في المصفوفة.
  6. سيُرجع تدفق بيانات الاستجابات المدموجة في استجابة واحدة، وسيُعاد مع الترويسة Content-Type بالقيمة text/html.

توجد طريقة أبسط لذلك وهي استخدام TransformStream، لكنها لا تُدعم في جميع المتصفحات، لذا سنعتمد حاليًا هذه الطريقة.

لنعد الآن إلى الحدث ()fetch في عامل الخدمة الذي كتبناه سابقًا، ونطبق داخله التابع ()mergeResponses:

self.addEventListener("fetch", event => {
  const { request } = event;

  // تم اختصار شيفرة معالجة الملفات الثابتة للتوضيح
  // ...

  // التحقق فيما إذا كان الطلب لمستند
  if (request.mode === "navigate") {
    // تم اختصار شيفرة التحميل المسبق/الجلب من الشبكة.
    // ...

    const { done, response } = await mergeResponses([
      caches.match("/partial-header"),
      networkContent,
      caches.match("/partial-footer")
    ]);

    event.waitUntil(done);
    event.respondWith(response);
  }
});

عند نهاية معالج الحدث ()fetch نمرر جزأي الترويسة والتذييل من CacheStorage إلى التابع ()mergeResponses، ونمرر النتيجة إلى تابع الحدث ()fetch، واسمه ()respondWith، الذي يخدم الاستجابة المدموجة نيابةً عن عامل الخدمة.

النتائج النهائية

نفذنا بالكثير من العمل المعقد والصعب نسبيًا، وقد لا يكون هذا الحل مناسبًا لبنية موقعك، لذا من المهم معرفة هل يعوض تحسن الأداء الناتج ذلك الجهد المبذول، وقد كانت مكاسب الأداء جيدةً في حالة موقع شركة ويكلي تيمبر:

fig-4.png

يظهر المخطط القيم الوسطية لكل من FPC و LCP لعدة أنواع من عامل الخدمة لموقع ويكلي تيمبر.

يقيس اختبار المحاكاة الأداء ضمن جهاز محدد وجودة اتصال معينة بالشبكة، وقد أجري الاختبار السابق على نسخة تجريبية من الموقع بمحاكاة لهاتف أندرويد نوكيا 2 واتصال "3G سريع" مخنوق داخل أدوات المطور في كروم، واختُبرت كل فئة عشر مرات على الصفحة الرئيسية للموقع، ونستنتج من ذلك ما يلي:

  • لا يوجد عامل خدمة أبدًا أسرع قليلًا من عامل الخدمة الأساسي الذي يستعمل طرائق بسيطة في التخزين المؤقت، وكلاهما أبطأ من عامل الخدمة الذي يبث البيانات، وقد يعود ذلك إلى عملية بدء عامل الخدمة مع ذلك ذلك ستظهر بيانات RUM (مراقبة المستخدم الحقيقية) التي سأعرضها بعد قليل حالة مختلفة.

عبد اللطيف ايمش: لم أفهم شيئًا منها

  • يرتبط كل من LCP و FCP ببعضهما عند عدم استخدام عامل خدمة، أو استخدام عامل خدمة "أساسي"، بسبب كون محتوى الصفحة بسيطًا وتنسيقات CSS صغيرةً للغاية، حيث تكون LCP عادةً الفقرة الافتتاحية داخل الصفحة.
  • تفصل خدمة تدفق البيانات داخل عامل الخدمة FCP عن LCP، لأن جزئية الترويسة تُرسل مباشرةً من CacheStroage.
  • تنقص قيمة كل من FCP وLCP عند تدفق البيانات داخل عامل الخدمة مقارنةً بالحالات الأخرى.

fig-5.png

يظهر المخطط القيم الوسطية لكل من FPC و LCP في بيانات الأداء في RUM (مراقبة المستخدم الحقيقي) لعدة أنواع من عامل الخدمة لموقع ويكلي تيمبر.

تظهر فائدة تدفق البيانات في عامل الخدمة لدى المستخدمين الحقيقيين، حيث لوحظ تحسن بقيمة 79% لقيمة FCP مقارنةً بعدم استخدام عامل خدمة أبدًا، وتحسن بقيمة 63% عن استخدام عامل الخدمة "الأساسي"، وقد تحسن الأداء في قيمة LCP أقل من ذلك، حيث لوحظ تحسن كبير بقيمة 41% مقارنةً بعدم استخدام عامل خدمة أبدًا، لكن كانت القيمة أبطأ قليلًا مقارنةً مع استخدام عامل الخدمة "الأساسي".

من المهم النظر إلى النسبة الكبيرة من بيانات الأداء المتوفرة، ولا يكفي النظر إلى المتوسطـ، لننظر إلى نسبة 95% من بيانات أداء FCP وLCP :

fig-6.png

مخطط لنسبة 95% من بيانات الأداء في RUM لكل من FCP و LCP لعدة أنواع من عامل الخدمة لموقع ويكلي تيمبر.

البيانات السابقة هي أفضل مكان يمكننا من خلاله استنتاج أبطأ أداء، ويمكننا ملاحظة تحسن بنسبة 40% و51% عند استخدام تدفق بيانات المحتوى ضمن عامل الخدمة لكل من FCP وLCP على التوالي مقارنةً بعدم استخدام عامل الخدمة، كما نلاحظ انخفاضَا لهاتين القيمتين بقيمة 19% و 43% على التوالي مقارنةً بعامل الخدمة "الأساسي"، وقد تلاحظ أن هذه البيانات غريبة بعض الشيء عن بيانات المحاكاة السابقة، ويجب أن تتذكر أن بيانات RUM تعتمد على زوّار موقعك، وهم يستخدمون شبكات اتصال متعددةً وأجهزةً مختلفةً.

استفاد كل من مؤشري FCP وLCP من الفوائد التي لا تحصى من فكرة تدفق بيانات المحتوى والتخزين المسبق -في حالة متصفح كروم-، والتخفيف من الترميز المرسل عبر الشبكة من خلال تجميع أجزاء الموقع من CacheStorage والشبكة، وأكبر مؤشر تأثر من ذلك هو FCP، يوضح الفيديو التالي الفروقات بين عدم استخدام عامل خدمة، وبين استخدام عامل الخدمة "الأساسي"، وبين استخدام تدفق البيانات والتخزين المسبق داخل عامل الخدمة:

fig-7.gif

تظهر الفيديوهات الثلاثة اختبارًا للزيارة المتكررة للصفحة الرئيسية في موقع ويكلي تيمبر، على اليسار صفحة لا يتحكم بها عامل الخدمة، فقط التخزين المسبق لـ HTTP، وتظهر على اليمين صفحتان يتحكم بهما عامل الخدمة، مع استخدام CacheStorage.

يمكننا أن نسأل أنفسنا الآن، بما أن هذه الطريقة حققت تحسن الأداء الكبير هذا مع موقع بسيط كهذا، فكيف سيكون التحسن مع المواقع الأكثر تعقيدًا، وماذا سنتوقع من موقع فيه قسم ترويسة وتذييل بحمولة ترميز أكبر من تلك؟

الخلاصة

لعملنا السابق مساوئ بلا شك، فمثلًا وضع الترويسة في التخزين المؤقت يعني أنه يجب تحديث عنوان المستند من خلال جافاسكريبت عند كل انتقال عبر تغيير قيمة document.title، كما يجب تعديل حالة التنقل من خلال جافاسكريبت أيضًا لتعكس الصفحة الحالية إذا كنت تنفذ ذلك داخل موقعك، لاحظ أن ذلك لن يؤثر على فهرسة موقعك لأن بوت جوجل Googlebot يزحف إلى الصفحات دون أن يحوي تخزينًا مؤقتًا.

قد تواجه تحديات أيضًا في المواقع التي تحوي على نظام مصادقة، فإذا كانت الترويسة داخل الموقع تظهر المستخدم الذي سجل الدخول حاليًا مثلًا، قد تحتاج لتحديث جزئية ترويسة الموقع الآتية من CacheStorage من خلال جافاسكريبت عند كل تنقّل لتعكس المستخدم الموثّق الحالي، ويمكنك ذلك مثلًا عبر تخزين بيانات أساسية عن المستخدم داخل localStorage، وتحديث الواجهة من تلك البيانات.

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

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

الواجهة البرمجية لعامل الخدمة أداة قوية لتقليل كمية الترميز المرسل عبر الشبكة، استفاد منها موقع شركة ويكلي تيمبر ومعظم المواقع التي توجهت لاستخدامه، فأصبح الموقع بسببه أسرع بكثير في أماكن بعيدة داخل ولاية ويسكونسون.

ترجمة -وبتصرف- للمقال Now THAT’S What I Call Service Worker لصاحبه Jeremy Wagner.

اقرأ أيضًا

 


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...