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