用 Service Worker 實現(xiàn)前端性能優(yōu)化
戳藍字「TianTianUp」關注我們哦!
作者:RetroAstro
https://github.com/RetroAstro/cosmos-blog
前言
說起前端性能優(yōu)化, 我們首先想到的可能就是用 Gulp 、Webpack 之類的自動化構建工具對 HTML、CSS 、JS 代碼進行壓縮,同時優(yōu)化圖片資源。再者就是使用 CSS Sprite 或者對于較小的圖片用 base64 直接編碼來進行優(yōu)化。當然還有很多可以優(yōu)化的方向,例如考慮瀏覽器緩存、頁面渲染性能 ( 減少重排與重繪與 GPU 硬件加速 ) 、JS阻塞性能等等。但我們今天講的是如何利用緩存策略在適宜的情況下直接減少對前端數(shù)據(jù)的請求量從而達到前端性能的優(yōu)化。因此 Service Worker 以及其相關的 API 就成為了我們今天的主角。
提醒 : 本篇文章將直接講述如何利用 Service Worker 對前端性能進行優(yōu)化,希望讀者在此之前已經(jīng)對 Service Worker 有基本的了解,若之前沒有接觸過,可以先看看以下的兩篇文章。
Service Worker ~ Google ( 墻 )
Service Worker 簡介
制定緩存策略
首先,既然是前端性能優(yōu)化,我們就需要想想該如何制定緩存策略才能達到理想的效果。我們可能有這樣的想法,即對 CSS 、JS 等易更改文件優(yōu)先使用網(wǎng)絡請求的數(shù)據(jù),而對于圖片資源則優(yōu)先使用緩存。如果再進一步思考的話,我們也許會希望在網(wǎng)絡條件好的情況下優(yōu)先使用網(wǎng)絡請求數(shù)據(jù),而網(wǎng)絡條件較差時則盡可能的直接使用緩存。嗯 ~ 看起來還不錯,那么根據(jù)以上的兩點我們先用代碼來實現(xiàn)一下吧。
先邁出最簡單的第一步,注冊 Service Worker。
// index.js
if ( 'serviceWorker' in navigator ) {
navigator.serviceWorker.register('/sw.js')
.then( registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch( err => console.log('ServiceWorker registration failed: ', err));
}
在 sw.js 中實現(xiàn)常規(guī)操作。
// sw.js
var cacheMaps = {
cache_file: 'css.js',
cache_image: 'images'
}
self.addEventListener('install', () => {
// 一般注冊以后,激活需要等到再次刷新頁面后再激活
// 可防止出現(xiàn)等待的情況,這意味著服務工作線程在安裝完后立即激活
self.skipWaiting();
})
// 運行觸發(fā)的事件
self.addEventListener('activate', event => {
event.waitUntil(
// 若緩存數(shù)據(jù)更改,則在這里更新緩存
caches.keys()
.then( cacheNames => {
return cacheNames.filter( item => !Object.values(cacheMaps).includes(item))
})
.then( keys => {
return Promise.all( keys.map( key => {
return caches.delete(key);
}))
})
// 更新客戶端上的 Service Worker 腳本
.then(() => self.clients.claim())
)
})
實現(xiàn)網(wǎng)絡優(yōu)先的邏輯。
function firstNet(cacheName, request) {
// 請求網(wǎng)絡數(shù)據(jù)并緩存
return fetch(request).then( response => {
var responseCopy = response.clone();
caches.open(cacheName).then( cache => {
cache.put(request, responseCopy);
});
return response;
}).catch(() => {
return caches.open(cacheName).then( cache => {
return cache.match(request);
});
});
}
實現(xiàn)緩存優(yōu)先的邏輯。
function firstCache(cacheName, request) {
return caches.open(cacheName).then( cache => {
return cache.match(request).then( response => {
var fetchServer = function() {
return fetch(request).then( newResponse => {
cache.put(request, newResponse.clone());
return newResponse;
});
}
// 如果緩存中有數(shù)據(jù)則返回,否則請求網(wǎng)絡數(shù)據(jù)
if (response) {
return response;
} else {
return fetchServer();
}
});
});
}
完成緩存策略中我們提到的第一點,即對 CSS 、JS 請求使用網(wǎng)絡優(yōu)先,圖片資源請求實現(xiàn)緩存優(yōu)先。
// sw.js
self.addEventListener('fetch', event => {
var
request = event.request,
url = request.url,
cacheName;
// 網(wǎng)絡優(yōu)先
if ( /\.(js|css)$/.test(url) ) {
(cacheName = cacheMaps.cache_file) && e.respondWith(firstNet(cacheName, request));
}
// 緩存優(yōu)先
else if ( /\.(png|jpg|jpeg|gif|webp)$/.test(url) ) {
(cacheName = cacheMaps.cache_image) && e.respondWith(firstCache(cacheName, request));
}
})
接下來我們利用 Promise.race() 完成一個競速模式,從而實現(xiàn)上文提到的第二點即根據(jù)網(wǎng)絡條件的好壞執(zhí)行相應的操作。
function networkCacheRace(cacheName, request) {
var timer, TIMEOUT = 500;
/**
* 網(wǎng)絡好的情況下給網(wǎng)絡請求500ms, 若超時則從緩存中取數(shù)據(jù)
* 若網(wǎng)絡較差且沒有緩存, 由于第一個 Promise 會一直處于 pending, 故此時等待網(wǎng)絡請求響應
*/
return Promise.race([new Promise((resolve, reject) => {
timer = setTimeout(() => {
caches.open(cacheName).then( cache => {
cache.match(request).then( response => {
if (response) {
resolve(response);
}
});
});
}, TIMEOUT);
}), fetch(request).then( response => {
clearTimeout(timer);
var responseCopy = response.clone();
caches.open(cacheName).then( cache => {
cache.put(request, responseCopy);
});
return response;
}).catch(() => {
clearTimeout(timer);
return caches.open(cacheName).then( cache => {
return cache.match(request);
});
})]);
}
現(xiàn)在我們可以在 sw.js 中更改一下緩存策略,從而達到最理想的效果。
// sw.js
self.addEventListener('fetch', event => {
// ...
if ( /\.(js|css)$/.test(url) ) {
(cacheName = cacheMaps.cache_file)
&& e.respondWith(networkCacheRace(cacheName, request));
}
// ...
})
更好的方案 - Workbox
什么是 Workbox ? 我們可以看看谷歌開發(fā)者官網(wǎng)中給出的解釋。
Workbox is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers.
其大概意思是它對常見的 Service Worker 操作進行了一層封裝, 根據(jù)最佳實踐方便了開發(fā)者的使用。因此在我們快速開發(fā)自己的 PWA 應用時使用 Workbox 是最合適不過的了。
它主要有以下幾大功能 :
Precaching ~ 預緩存
Runtime caching ~ 運行時緩存
Strategies ~ 緩存策略
Request routing ~ 請求路由控制
Background sync ~ 后臺同步
etc …
基于本文的內(nèi)容, 在這里我們只談談如何簡單的使用 Workbox 以及它所提供的幾種緩存策略。
注意在 index.js 里面的注冊操作不會改變, 變化的是 sw.js 中的代碼。
// sw.js
// 導入谷歌提供的 Workbox 庫
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.2.0/workbox-sw.js');
if ( !workbox ) {
console.log(`Workbox didn't load.`);
return;
}
// Workbox 注冊成功, 可以進行下一步的操作
// 立即激活, 跳過等待
workbox.skipWaiting();
workbox.clientsClaim();
// workbox.routing.registerRoute()...
下面用官網(wǎng)給出的幾張圖解釋一下 Workbox 所提供的幾種緩存策略,而它們正好能滿足上文我們自己用代碼所實現(xiàn)的效果。
Stale-While-Revalidate

Cache First

Network First

Cache Only

Network Only

接下來讓我們使用 Workbox 去實現(xiàn)上文優(yōu)化前端性能的緩存策略。
緩存優(yōu)先 :
workbox.routing.registerRoute(
/\.(png|jpg|jpeg|gif|webp)$/,
// 對于圖片資源使用緩存優(yōu)先
workbox.strategies.cacheFirst({
cacheName: 'images',
// 設置最大緩存數(shù)量以及過期時間
plugins: [
new workbox.expiration.Plugin({
maxEntries: 60,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
}),
);
網(wǎng)絡優(yōu)先 :
workbox.routing.registerRoute(
/\.(js|css)$/,
workbox.strategies.staleWhileRevalidate({
cacheName: 'css.js',
}),
);
由上文圖中可看出 stale-while-revalidate 策略與我們實現(xiàn)的網(wǎng)絡優(yōu)先稍有不同,確切的來說更加明智,因為除了第一次需要網(wǎng)絡請求,接下來的請求會直接從緩存中取數(shù)據(jù)但在頁面加載之后會立即更新緩存,這樣既保證了加載速度又能每次將數(shù)據(jù)準確的更新到最新版本。
競速模式 :
workbox.routing.registerRoute(
/\.(js|css)$/,
workbox.strategies.networkFirst({
// 給網(wǎng)絡請求0.5秒,若仍未返回則從緩存中取數(shù)據(jù)
networkTimetoutSeconds: 0.5,
cacheName: 'css.js',
}),
);
回頭看看我們手動實現(xiàn)的緩存策略,顯然使用 Workbox 要簡單的多。當然 Workbox 中還有很多東西需要注意,但由于已經(jīng)超出了文章所講的主要內(nèi)容因此在這里無法具體闡述,建議讀者還是到官網(wǎng)去仔細看看文檔詳細了解一下,若因為墻的問題可以看看第二篇文章。
Workbox ~ Google ( 墻 )
神奇的 Workbox 3.0
---------- END ----------


