Предварительная загрузка навигации позволяет сократить время запуска Service Worker за счет параллельного выполнения запросов.
Краткое содержание
- В некоторых ситуациях время загрузки Service Worker может задержать ответ сети .
- Предварительная загрузка навигации , доступная в трех основных браузерных движках, исправляет эту проблему, позволяя выполнять запрос параллельно с загрузкой сервис-воркера.
- Вы можете отличить запросы предварительной загрузки от обычных переходов с помощью заголовка и обслуживать другой контент .
Проблема
Когда вы переходите на сайт, который использует сервис-воркера для обработки событий выборки, браузер запрашивает у сервис-воркера ответ. Это включает в себя загрузку сервисного работника (если он еще не запущен) и отправку события выборки.
Время загрузки зависит от устройства и условий. Обычно это около 50 мс. На мобильном телефоне это больше похоже на 250 мс. В крайних случаях (медленные устройства, сбой в работе процессора) оно может превышать 500 мс. Однако, поскольку работник службы бодрствует в течение определенного браузером времени между событиями, такая задержка возникает только изредка, например, когда пользователь переходит на ваш сайт с новой вкладки или другого сайта.
Время загрузки не является проблемой, если вы отвечаете из кэша, поскольку преимущество пропуска сети превышает задержку загрузки. Но если вы отвечаете через сеть…
Сетевой запрос задерживается из-за загрузки сервисного работника.
Мы продолжаем сокращать время загрузки , используя кэширование кода в V8 , пропуская сервис-воркеры, у которых нет события выборки , спекулятивный запуск сервис-воркеров и другие оптимизации. Однако время загрузки всегда будет больше нуля.
Facebook обратил наше внимание на влияние этой проблемы и попросил указать способ параллельного выполнения навигационных запросов:
Предварительная загрузка навигации спешит на помощь
Предварительная загрузка навигации — это функция, которая позволяет вам сказать: «Когда пользователь отправляет запрос навигации 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
});
Большое спасибо Мэтту Фалькенхагену и Цуёси Хоро за работу над этой функцией и помощь в написании этой статьи. И огромное спасибо всем, кто участвует в усилиях по стандартизации.