يتيح لك التحميل المُسبق للتنقّل التغلب على وقت بدء تشغيل مشغّل الخدمات من خلال تقديم طلبات بالتوازي.
ملخّص
- في بعض الحالات، قد يؤدي وقت تشغيل مشغّل الخدمات إلى تأخير استجابة الشبكة.
- تعمل ميزة التحميل المسبق للتنقّل المتاحة في المحركات الرئيسية الثلاثة للمتصفّحات على إصلاح هذه المشكلة من خلال السماح لك بإنشاء الطلب بالتوازي مع تشغيل عامل الخدمة.
- يمكنك استخدام العنوان للتمييز بين طلبات التحميل المُسبق وعمليات التنقّل العادية باستخدام العنوان، وعرض محتوى مختلف.
المشكلة
عند الانتقال إلى موقع إلكتروني يستخدم مشغّل خدمات لمعالجة أحداث الجلب، يطلب المتصفّح من عامل الخدمة الردّ. ويشمل ذلك بدء تشغيل مشغّل الخدمات (إذا لم يكن قيد التشغيل)، وإرسال حدث الجلب.
يعتمد وقت بدء التشغيل على الجهاز والشروط. وهي تبلغ عادةً حوالي 50 ملي ثانية. وعلى الأجهزة الجوّالة، تكون المدة مثل 250 ملي ثانية تقريبًا. في الحالات القصوى (الأجهزة البطيئة ووحدة المعالجة المركزية التي تعاني من استياء) يمكن أن تزيد مدة تشغيلها عن 500 ملي ثانية. ومع ذلك، لن تحصل على هذا التأخير إلا من حين لآخر، مثل انتقال المستخدِم إلى موقعك الإلكتروني من علامة تبويب جديدة أو موقع إلكتروني آخر، وذلك لأنّ مشغّل الخدمات يظل نشطًا خلال فترة يحدِّدها المتصفِّح بين الأحداث.
لا يمثل وقت بدء التشغيل أي مشكلة إذا كنت تستجيب من ذاكرة التخزين المؤقت، حيث إن فائدة تخطي الشبكة أكبر من تأخير بدء التشغيل. ولكن إذا كنت تستجيب باستخدام الشبكة...
يتأخر طلب الشبكة بسبب تشغيل عامل الخدمة.
ونواصل تقليل وقت التشغيل من خلال استخدام التخزين المؤقت للرموز في الإصدار 8، أو من خلال تخطّي موظفي الخدمات الذين ليس لديهم حدث جلب أو إطلاق مشغِّلي الخدمات بناءً على توقُّع وغير ذلك من التحسينات. ومع ذلك، ستكون مدة التمهيد دائمًا أكبر من صفر.
لفتت Facebook انتباهنا إلى تأثير هذه المشكلة، وطلبت طريقة لتنفيذ طلبات التنقل بالتوازي:
التحميل المُسبق للتنقّل عبر عملية الإنقاذ
التحميل المُسبَق للتنقل هو ميزة تتيح لك أن تقول "عندما يُجري المستخدم طلب تنقُّل عبر GET، عليك بدء طلب الشبكة أثناء تشغيل عامل الخدمة".
لا يزال تأخير بدء التشغيل موجودًا، لكنه لا يحظر طلب الشبكة، لذلك يحصل المستخدم على المحتوى بشكل أسرع.
وإليك فيديو لهذه العملية حيث يتم منح عامل الخدمة مهلة متعمّدة في بدء التشغيل لمدة 500 ملي ثانية باستخدام حلقة "أثناء التشغيل":
إليك العرض التوضيحي نفسه. للحصول على مزايا التحميل المُسبق للتنقّل، ستحتاج إلى متصفّح متوافق معها.
تفعيل التحميل المُسبَق للتنقّل
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
});
نتوجّه بالشكر الجزيل إلى "مات فالكنهاغن" و"تسويوشي هورو" على عملهما على هذه الميزة والمساعدة في شرح هذه المقالة. شكرًا جزيلاً لجميع المشاركين في جهود توحيد المقاييس