资讯专栏INFORMATION COLUMN

用 Service Worker 实现前端性能优化

yuanxin / 1379人阅读

摘要:导入谷歌提供的库注册成功可以进行下一步的操作立即激活跳过等待下面用官网给出的几张图解释一下所提供的几种缓存策略而它们正好能满足上文我们自己用代码所实现的效果。接下来让我们使用去实现上文优化前端性能的缓存策略。

前言 :

说起前端性能优化, 我们首先想到的可能就是用 Gulp 、Webpack 之类的自动化构建工具对 HTML、CSS 、JS 代码进行压缩,同时优化图片资源。再者就是使用 CSS Sprite 或者对于较小的图片用 base64 直接编码来进行优化。当然还有很多可以优化的方向, 例如考虑浏览器缓存、页面渲染性能 ( 减少重排与重绘和 GPU 硬件加速 ) 、JS阻塞性能等等。但我们今天讲的是如何利用缓存策略在适宜的情况下直接减少对前端数据的请求量从而达到前端性能的优化。因此 Service Worker 以及其相关的 API 就成为了我们今天的主角。

提醒 : 本篇文章将直接讲述如何利用 Service Worker 对前端性能进行优化, 希望读者在此之前已经对 Service Worker 有基本的了解, 若之前没有接触过, 可以先看看以下的两篇文章。

Service Worker ~ Google ( 墙 )

Service Worker 简介

制定缓存策略

首先, 既然是前端性能优化, 我们就需要想想该如何制定缓存策略才能达到理想的效果。我们可能有这样的想法, 即对 CSS 、JS 等易更改文件优先使用网络请求的数据, 而对于图片资源则优先使用缓存。如果再进一步思考的话, 我们也许会希望在网络条件好的情况下优先使用网络请求数据, 而网络条件较差时则尽可能的直接使用缓存。嗯 ~ 看起来还不错, 那么根据以上的两点我们先用代码来实现一下吧。

先迈出最简单的第一步, 注册 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 中实现常规操作。

// 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

Cache First

Network First

Cache Only

Network Only

接下来让我们使用 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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95603.html

相关文章

  • 前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【下篇】

    摘要:安装后已经完成了安装,并且等待其他的线程被关闭。激活后在这个状态会处理事件回调提供了更新缓存策略的机会。并可以处理功能性的事件请求后台同步推送。废弃状态这个状态表示一个的生命周期结束。 showImg(https://segmentfault.com/img/bVbwWJu?w=2056&h=1536); 不知不觉,已经来到了最后的下篇 其实我写的东西你如果认真去看,跟着去写,应该能有...

    fireflow 评论0 收藏0
  • 前端每周清单半年盘点之 PWA 篇

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢迎关注【前端之巅】微信公众号(ID:frontshow),及时获取前端每周清单;本文则是对于...

    崔晓明 评论0 收藏0
  • 前端优化 - 收藏集 - 掘金

    摘要:虽然有着各种各样的不同,但是相同的是,他们前端优化不完全指南前端掘金篇幅可能有点长,我想先聊一聊阅读的方式,我希望你阅读的时候,能够把我当作你的竞争对手,你的梦想是超越我。 如何提升页面渲染效率 - 前端 - 掘金Web页面的性能 我们每天都会浏览很多的Web页面,使用很多基于Web的应用。这些站点看起来既不一样,用途也都各有不同,有在线视频,Social Media,新闻,邮件客户端...

    VincentFF 评论0 收藏0
  • 如何优化你的超大型React应 【原创精读】

    摘要:往往纯的单页面应用一般不会太复杂,所以这里不引入和等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。构建极度复杂,超大数据的应用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React为了大型应用而生,Electron和React-native赋予了它构建移动端跨平台App和桌面应用的能力,Taro则赋...

    cfanr 评论0 收藏0
  • 如何优化你的超大型React应 【原创精读】

    摘要:往往纯的单页面应用一般不会太复杂,所以这里不引入和等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。构建极度复杂,超大数据的应用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React为了大型应用而生,Electron和React-native赋予了它构建移动端跨平台App和桌面应用的能力,Taro则赋...

    codecook 评论0 收藏0

发表评论

0条评论

yuanxin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<