用 Service Worker 實現前端性能優化

前言 :

提及前端性能優化, 咱們首先想到的可能就是用 Gulp 、Webpack 之類的自動化構建工具對 HTML、CSS 、JS 代碼進行壓縮,同時優化圖片資源。再者就是使用 CSS Sprite 或者對於較小的圖片用 base64 直接編碼來進行優化。固然還有不少能夠優化的方向, 例如考慮瀏覽器緩存、頁面渲染性能 ( 減小重排與重繪和 GPU 硬件加速 ) 、JS阻塞性能等等。但咱們今天講的是如何利用緩存策略在適宜的狀況下直接減小對前端數據的請求量從而達到前端性能的優化。所以 Service Worker 以及其相關的 API 就成爲了咱們今天的主角。javascript

提醒 : 本篇文章將直接講述如何利用 Service Worker 對前端性能進行優化, 但願讀者在此以前已經對 Service Worker 有基本的瞭解, 若以前沒有接觸過, 能夠先看看如下的兩篇文章。

Service Worker ~ Google ( 牆 )css

Service Worker 簡介前端

制定緩存策略

首先, 既然是前端性能優化, 咱們就須要想一想該如何制定緩存策略才能達到理想的效果。咱們可能有這樣的想法, 即對 CSS 、JS 等易更改文件優先使用網絡請求的數據, 而對於圖片資源則優先使用緩存。若是再進一步思考的話, 咱們也許會但願在網絡條件好的狀況下優先使用網絡請求數據, 而網絡條件較差時則儘量的直接使用緩存。嗯 ~ 看起來還不錯, 那麼根據以上的兩點咱們先用代碼來實現一下吧。java

先邁出最簡單的第一步, 註冊 Service Worker。web

// 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 中實現常規操做。api

// sw.js

var cacheMaps = {
    cache_file: 'css.js',
    cache_image: 'images'
}

self.addEventListener('install', () => {
    // 通常註冊之後,激活須要等到再次刷新頁面後再激活
    // 可防止出現等待的狀況,這意味着服務工做線程在安裝完後當即激活
    self.skipWaiting();
})

// 運行觸發的事件
self.addEventListener('activate', event => {
    event.waitUntil(
        // 若緩存數據更改,則在這裏更新緩存
        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())
    )
})

實現網絡優先的邏輯。瀏覽器

function firstNet(cacheName, request) {
    // 請求網絡數據並緩存
    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);
        });
    });
}

實現緩存優先的邏輯。緩存

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;
                });
            }
            // 若是緩存中有數據則返回,不然請求網絡數據
            if (response) {
                return response;
            } else {
                return fetchServer();
            }
        });
    });
}

完成緩存策略中咱們提到的第一點,即對 CSS 、JS 請求使用網絡優先,圖片資源請求實現緩存優先。性能優化

// sw.js

self.addEventListener('fetch', event => {
    var 
    request = event.request,
    url = request.url,
    cacheName;
  
    // 網絡優先
    if ( /\.(js|css)$/.test(url) ) {
        (cacheName = cacheMaps.cache_file) && e.respondWith(firstNet(cacheName, request));
    }
    // 緩存優先
    else if ( /\.(png|jpg|jpeg|gif|webp)$/.test(url) ) {
        (cacheName = cacheMaps.cache_image) && e.respondWith(firstCache(cacheName, request));
    }
})

接下來咱們利用 Promise.race() 完成一個競速模式, 從而實現上文提到的第二點即根據網絡條件的好壞執行相應的操做。網絡

function networkCacheRace(cacheName, request) {
    var timer, TIMEOUT = 500;
    /**
     * 網絡好的狀況下給網絡請求500ms, 若超時則從緩存中取數據
     * 若網絡較差且沒有緩存, 因爲第一個Promise會一直處於pending, 故此時等待網絡請求響應
     */
    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);
        });
    })]);
}

如今咱們能夠在 sw.js 中更改一下緩存策略,從而達到最理想的效果。

// sw.js

self.addEventListener('fetch', event => {
    // ...
    if ( /\.(js|css)$/.test(url) ) {
        (cacheName = cacheMaps.cache_file) 
        && e.respondWith(networkCacheRace(cacheName, request));
    }
    // ...
})

更好的方案 : Workbox

什麼是 Workbox ? 咱們能夠看看谷歌開發者官網中給出的解釋。

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 操做進行了一層封裝, 根據最佳實踐方便了開發者的使用。所以在咱們快速開發本身的 PWA 應用時使用 Workbox 是最合適不過的了。

它主要有如下幾大功能 :

  • Precaching ~ 預緩存
  • Runtime caching ~ 運行時緩存
  • Strategies ~ 緩存策略
  • Request routing ~ 請求路由控制
  • Background sync ~ 後臺同步
  • etc ...

基於本文的內容, 在這裏咱們只談談如何簡單的使用 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()...

下面用官網給出的幾張圖解釋一下 Workbox 所提供的幾種緩存策略, 而它們正好能知足上文咱們本身用代碼所實現的效果。

Stale-While-Revalidate

1

Cache First

2

Network First

3

Cache Only

4

Network Only

5

接下來讓咱們使用 Workbox 去實現上文優化前端性能的緩存策略。

緩存優先 :

workbox.routing.registerRoute(
  /\.(png|jpg|jpeg|gif|webp)$/,
  // 對於圖片資源使用緩存優先
  workbox.strategies.cacheFirst({
    cacheName: 'images',
    // 設置最大緩存數量以及過時時間
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 60,
        maxAgeSeconds: 7 * 24 * 60 * 60,
      }),
    ],
  }),
);

網絡優先 :

workbox.routing.registerRoute(
  /\.(js|css)$/,
  workbox.strategies.staleWhileRevalidate({
    cacheName: 'css.js',
  }),
);

由上文圖中可看出 stale-while-revalidate 策略與咱們實現的網絡優先稍有不一樣, 確切的來講更加明智, 由於除了第一次須要網絡請求, 接下來的請求會直接從緩存中取數據但在頁面加載以後會當即更新緩存, 這樣既保證了加載速度又能每次將數據準確的更新到最新版本。

競速模式 :

workbox.routing.registerRoute(
    /\.(js|css)$/,
    workbox.strategies.networkFirst({
        // 給網絡請求0.5秒,若仍未返回則從緩存中取數據
        networkTimetoutSeconds: 0.5,
        cacheName: 'css.js',
    }),
);

回頭看看咱們手動實現的緩存策略, 顯然使用 Workbox 要簡單的多。固然 Workbox 中還有不少東西須要注意, 但因爲已經超出了文章所講的主要內容所以在這裏沒法具體闡述, 建議讀者仍是到官網去仔細看看文檔詳細瞭解一下,若由於牆的問題能夠看看第二篇文章。

Workbox ~ Google ( 牆 )

神奇的 Workbox 3.0

相關文章
相關標籤/搜索