乐趣区

关于javascript:前端缓存机制提升网站性能-Service-Worker

PWA 简介

PWA(Progressive Web Apps)不是特指某一项技术,而是利用多项技术来改善用户体验的 Web App,为 Web App 提供相似 Native App 的用户体验。
其核心技术包含 Web App Manifest,Web Push,Service Worker 和 Cache Api 等,用户体验才是 PWA 的外围。
PWA 次要特点如下:

  • 牢靠 – 即便在 网络不稳固甚至断网的环境下,也能霎时加载并展示
  • 用户体验 – 疾速响应,具备平滑的过渡动画及用户操作的反馈
  • 用户黏性 – 和 Native App 一样,能够被增加到桌面,能承受离线告诉,具备沉迷式的用户体验

    写在后面

  • 文章不具体解说 PWA 技术细节,如果对 PWA 技术感兴趣,文末筹备了一些材料,能够参考学习
  • 此次调研目标并非为网站残缺接入 PWA 技术,而是利用其缓存机制晋升网站性能
  • 次要用到的技术为 Service Worker + Cache Api

    前端多级缓存模型

    当浏览器想要获取近程的数据时,咱们并不会立刻出发(发送申请),在计算机领域,很多性能问题都会通过减少缓存来解决,前端也不例外。
    和许多后端服务一样,前端缓存也是多级的。

  • 本地读取阶段,这个阶段咱们不会发动任何 HTTP 申请,只在本地读取数据作为响应
  • HTTP request 阶段,这个阶段咱们发动了 HTTP 申请,然而数据仍然是从本地读取。目前为止,咱们可能还没有收回一个真正的申请。这也意味着,在缓存查看阶段咱们就会有很多机会将后续的性能问题扼杀在摇篮之中
  • 真正申请阶段,如果很可怜本地没有任何无效数据,这时候才会发动真正的申请
    前端多级缓存具体流程图如下:

    有了 HTTP 缓存为什么还须要 Service Worker?

Service worker 除了针对 PWA(推送和音讯)以外,对一般 web 来说,在缓存方面,能比 http 缓存带来一些额定的益处。
能够了解为,SW 就是浏览器把缓存治理凋谢一层 接口 给开发者。
劣势如下:
1、改写默认行为。
例如,浏览器默认在刷新时,会对所有资源都从新发动申请,即便缓存还是有效期内,而应用了 SW,就能够改写这个行为,间接返回缓存。
2、缓存和更新并存。
要让网页离线应用,就须要整站应用长缓存,包含 HTML。而 HTML 应用了长缓存,就无奈及时更新(浏览器没有凋谢接口间接删除某个 html 缓存)。而应用 SW 就能够,每次先应用缓存局部,而后再发动 SW js 的申请,这个申请咱们能够施行变更,批改 HTML 版本,从新缓存一份。那么用户下次关上就能够看到新版本了。
3、无侵入式。
无侵入式版本控制。最优的版本控制,个别是 HTML 中记录所有 js css 的文件名(HASH),而后按需发动申请。每个资源都长缓存。而这个过程,就须要扭转了我的项目构造,至多多一个 js 或者一段 js 管制版本号,发动申请时还须要 url 中注入冬天的文件名。应用了 SW,就能够把这部分非业务逻辑整合到 sw js 中。
无侵入式申请统计。例如缓存比例统计、图片 404 统计。
4、额定缓存。
HTTP 缓存空间无限,容易被冲掉。尽管局部浏览器实现 SW 的存储也有淘汰机制,但多一层缓存,命中的概率就要更高了。
5、离线解决。
当监测到离线,而且又没有缓存某个图片时,能够做非凡解决,返回离线的提醒。又或者做一个纯前端的 404/ 断网页面。相似 Chrome 的小恐龙页面。
6、预加载资源。
这个相似 prefetch 标签。
7、前置解决。
例如校验 html/JS 是否被运营商劫持?js 文件到了 UI 过程执行后,就无奈删除恶意代码,而在 SW 中,咱们能够当作文本一样,轻松解决。当然,在 HTTPS 环境下呈现劫持的概率是极低的。
起源:https://www.cnblogs.com/kenko…

Service Worker

简介

Service Worker 的初衷是极致优化用户体验,带来丝滑般晦涩的离线利用。但同时也能够用作站点缓存应用。它自身相似于一个介于浏览器和服务端之间的网络代理,能够拦挡申请并操作响应内容。
Service Worker 在 Web Worker 的根底上加上了长久离线缓存能力,能够通过本身的 生命周期 个性保障简单的工作只解决一次,并长久缓存处理结果,直到批改了 Service Worker 的外在的解决逻辑。
特点总结如下:

  • 一个非凡的 worker 线程,独立于以后网页主线程,有本人的执行上下文
  • 一旦被装置,就永远存在,除非显示勾销注册
  • 应用到的时候浏览器会主动唤醒,不必的时候主动休眠
  • 可拦挡并代理申请和解决返回,能够操作本地缓存,如 CacheStorage,IndexedDB 等
  • 离线内容开发者可控
  • 能承受服务器推送的离线音讯
  • 异步实现,外部接口异步化根本是通过 Promise 实现
  • 不能间接操作 DOM
  • 必须在 HTTPS 环境下能力工作

    使用者

    有很多团队也是启用该工具来实现 Service Worker 的缓存,比如说:

  • 淘宝首页
  • 网易新闻 wap 文章页
  • 百度的 Lavas
  • fullstory
    … …

兼容性

如下图所示,除了 IE 和 Opera Mini 不反对,大部分古代浏览器都没有问题,兼容度超过 96%

安全性

Service Worker 是一种独立于浏览器主线程的 工作线程 ,与以后的浏览器主线程是齐全隔离的,并有本人独立的执行上下文(context)。因为 Service Worker 线程是独立于主线程的工作线程,所以在 Service Worker 中的任何操作都不会影响到主线程。
因而,在浏览器不反对 Service Worker、Service Worker 挂掉和 Service Worker 出错等等状况下,主体网站都不会受到影响,因而从网站故障角度讲是 100% 平安的。
其可能呈现问题的中央在于数据的准确性,这波及到缓存策略和淘汰算法等技术,也是配置 Service Worker 的重点。

作用域

Service Worker 注册会有意想不到的作用域净化问题
SPA 在工程架构上只有一个 index.html 的入口,站点的内容都是异步申请数据之后在前端渲染的,利用中的页面切换都是在前端路由管制的。
通常会将这个 index.html 部署到 https://somehost,SPA 的 Service Worker 只须要在 index.html 中注册一次。所以个别会将 sw.js 间接放在站点的根目录保障可拜访,也就是说 Service Worker 的作用域通常就是 /,这样 Service Worker 可能管制 index.html,从而管制整个 SPA 的缓存。

代码如下:

  var sp = window.location.protocol + '//' + window.location.host + '/';
  if ('serviceWorker' in navigator) {
    // 为了避免作用域净化,将装置前登记所有已失效的 Service Worker
    navigator.serviceWorker.getRegistrations().then(regs => {for (let reg of regs) {reg.unregister();
      }
      navigator.serviceWorker
        .register(sp + 'service-worker.js', {scope: sp,})
        .then(reg => {console.log('set scope:', sp, 'service worker instance:', reg)
        });
    });
  }

更新

在执行 navigator.serviceWorker.register() 办法注册 Service Worker 的时候,浏览器通过本身 diff 算法可能检测 sw.js 的更新蕴含两种形式:

  • Service Worker 文件 URL 的更新
  • Service Worker 文件内容的更新

在理论我的项目中,在 Web App 新上线的时候,通常是在注册 Service Worker 的时候,通过批改 Service Worker 文件的 URL 来进行 Service Worker 的更新,这部分工作能够通过 webpack 插件实现

缓存策略

预缓存

动态资源具备确定性,因而能够被动获取所需缓存的资源列表,并且在 Service Worker 装置阶段就被动发动动态资源申请并缓存,这样一旦新的 Service Worker 被激活之后,缓存就间接能投入使用了。这是一个资源预取的过程,因而动态资源的缓存计划也称为预缓存计划。对于预缓存更多细节能够参考预缓存计划

动静缓存

在 Service Worker 环境下,能够通过 Fetch API 发送网络申请获取资源,也能够通过 Cache API、IndexedDB 等本地缓存中获取缓存资源,甚至能够在 Service Worker 间接生成一个 Response 对象,以上这些都属于资源响应的起源。资源申请响应策略的作用,就是用来解决响应的资源从哪里来的问题。更多申请响应策略参考这里

一些倡议

  1. HTML,如果你想让页面离线能够拜访,应用 NetworkFirst,如果不须要离线拜访,应用 NetworkOnly,其余策略均不倡议对 HTML 应用
  2. CSS 和 JS,状况比较复杂,因为个别站点的 CSS,JS 都在 CDN 上,SW 并没有方法判断从 CDN 上申请下来的资源是否正确(HTTP 200),如果缓存了失败的后果,问题就大了。这种我倡议应用 Stale-While-Revalidate 策略,既保证了页面速度,即使失败,用户刷新一下就更新了
  3. 如果你的 CSS,JS 与站点在同一个域下,并且文件名中带了 Hash 版本号,那能够间接应用 Cache First 策略
  4. 图片倡议应用 Cache First,并设置肯定的生效事件,申请一次就不会再变动了
  5. 所有接口类缓存都倡议 Stale-While-Revalidate 策略
  6. 对于不在同一域下的任何资源,相对不能应用 Cache only 和 Cache first。

更多缓存策略相干,能够在上面文章查看:
PWA 之 Workbox 缓存策略剖析
Service Worker 开发工具

生命周期

对于 Service Worker 生命周期相干的,次要是波及 Service Worker 本身的更新和在什么阶段缓存对应的资源。更多信息点击这里

Cache API

离线存储计划比照

前端支流离线存储计划比照如下所示:

Cache API 是为资源申请与响应的存储量身定做的,它采纳了键值对的数据模型存储格局,以申请对象为键、响应对象为值,正好对应了发动网络资源申请时申请与响应一一对应的关系。因而 Cache API 实用于申请响应的本地存储。

IndexedDB 则是一种非关系型(NoSQL)数据库,它的存储对象次要是数据,比方数字、字符串、Plain Objects、Array 等,以及大量非凡对象比方 Date、RegExp、Map、Set 等等,对于 Request、Response 这些是无奈间接被 IndexedDB 存储的。

能够看到,Cache API 和 IndexedDB 在性能上是互补的。在设计本地资源缓存计划时通常以 Cache API 为主,但在一些简单的场景下,Cache API 这种申请与响应一一对应的模式存在着局限性,因而须要联合上性能上更为灵便的 IndexedDB,通过 IndexedDB 存取一些要害的数据信息,辅助 Cache API 进行资源管理。

兼容性


总结

通过上述比照,咱们能够应用 IndexedDB 及 CacheStorage 来为 Service Worker 的离线存储提供底层服务,依据社区的教训,它们各自的实用场景为:

  • 对于网址可寻址的(比方脚本、款式、图片、HTML 等)资源应用 CacheStorage
  • 其余资源则应用 IndexedDB

Workbox

简介

在页面线程中,尽管能够间接应用底层 API 来解决 Service Worker 的注册、更新与通信,但在较为简单的利用场景下(比方,页面中不同窗口注册不同的 Service Worker),咱们往往会因为要解决各种状况而逐渐陷入简单、凌乱的深渊,并且,在呈现运行后果与预期后果不统一时,咱们往往手足无措、不知如何进行排查。正是因为这些起因,Google Chrome 团队推出的一套 PWA 的解决方案 Workbox,这套解决方案当中蕴含了外围库和构建工具,因而咱们能够利用 Workbox 实现 Service Worker 的疾速开发。

webpack 插件

官网提供 workbox-webpack-plugin 插件为咱们进一步节俭开发成本(版本 v6.4.2)

为什么须要这个 webpack 插件?

  • 给预缓存打 hash,开发的时候动静更新 hash
  • 更不便的接口去动静缓存配置形式,主动生成和更新 sw

接入代码

const {InjectManifest} = require('workbox-webpack-plugin');
 // 注入模式
new InjectManifest({swSrc: path.resolve(__dirname, 'src/service-worker.js'), // 已有 SW 门路
  swDest: 'service-worker.js', // 指标文件名(打包后)maximumFileSizeToCacheInBytes: 1024000 * 4, // 只缓存 4M 以下的文件
  include: [/.*.(png|jpg|jpeg|svg|ico|webp)$/, 'beautify.js'], // 仅蕴含图片和 beautify.js
}),

service-worker.js 残缺代码

// 根底配置
import {setCacheNameDetails, skipWaiting, clientsClaim} from 'workbox-core';
// 缓存相干
import {precacheAndRoute} from 'workbox-precaching/precacheAndRoute';
import {registerRoute, setDefaultHandler} from 'workbox-routing';
import {
  NetworkFirst,
  StaleWhileRevalidate,
  CacheFirst,
  NetworkOnly,
} from 'workbox-strategies';
// 插件
import {CacheableResponsePlugin} from 'workbox-cacheable-response/CacheableResponsePlugin';
import {ExpirationPlugin} from 'workbox-expiration/ExpirationPlugin';
// 内置计划
import {pageCache, offlineFallback} from 'workbox-recipes';

// 自定义插件去掉申请参数 t
async function cacheKeyWillBeUsed({request}) {const url = new URL(request.url);
  // Remove only paramToBeIgnored and keep other URL parameters.
  url.searchParams.delete('t');
  // Return the full remaining href.
  return url.href;
}

setCacheNameDetails({
  prefix: 'sw-tools',
  suffix: 'v1',
  precache: 'precache',
  runtime: 'runtime-cache',
});

skipWaiting();
clientsClaim();
/*
 通常当用户拜访 / 时,对应的拜访的页面 HTML 文件是 /index.html,默认状况下,precache 路由机制会在任何 URL 的结尾的 / 后加上 index.html,这就认为着你预缓存的任何 index.html 都能够通过 /index.html 或者 / 拜访到。当然,你也能够通过 directoryIndex 参数禁用掉这个默认行为
 */
precacheAndRoute(self.__WB_MANIFEST, {ignoreUrlParametersMatching: [/.*/],
  directoryIndex: null,
});

// 离线页面缓存
offlineFallback();
// URL navigation 缓存
pageCache();

// html 的缓存
// HTML,如果你想让页面离线能够拜访,应用 NetworkFirst,如果不须要离线拜访,应用 NetworkOnly,其余策略均不倡议对 HTML 应用。registerRoute(new RegExp(/.*\.html/), new NetworkFirst());

// 动态资源的缓存
//CSS 和 JS,状况比较复杂,因为个别站点的 CSS,JS 都在 CDN 上,SW 并没有方法判断从 CDN 上申请下来的资源是否正确(HTTP 200),如果缓存了失败的后果,问题就大了。这种我倡议应用 Stale-While-Revalidate 策略,既保证了页面速度,即使失败,用户刷新一下就更新了。如果你的 CSS,JS 与站点在同一个域下,并且文件名中带了 Hash 版本号,那能够间接应用 Cache First 策略。const staticMatchCallback = ({request}) =>
  // CSS
  request.destination === 'style' ||
  // JavaScript
  request.destination === 'script' ||
  // Web Workers
  request.destination === 'worker';
registerRoute(
  staticMatchCallback,
  new StaleWhileRevalidate({
    cacheName: 'static-resources',
    plugins: [
      new CacheableResponsePlugin({statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 500,
        maxAgeSeconds: 30 * 24 * 60 * 60,
        purgeOnQuotaError: true,
      }),
    ],
  }),
);

// 图片的缓存
// 图片倡议应用 Cache First,并设置肯定的生效事件,申请一次就不会再变动了。registerRoute(({ request}) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new CacheableResponsePlugin({statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 200,
        maxAgeSeconds: 30 * 24 * 60 * 60,
        purgeOnQuotaError: true,
      }),
    ],
  }),
);

// 事件流接口的缓存
registerRoute(/^http(s)?:\/\/((dev\.)|(test\.)|(testing\.))?xxxx.net\/api\/v\d+\/(.*)?\/session_events.*/,
  new StaleWhileRevalidate({
    cacheName: 'session_events_cache',
    plugins: [{ cacheKeyWillBeUsed},
      new CacheableResponsePlugin({statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 2000,
        maxAgeSeconds: 7 * 24 * 60 * 60,
        purgeOnQuotaError: true,
      }),
    ],
  }),
);

// play/init 接口
registerRoute(/^http(s)?:\/\/((dev\.)|(test\.)|(testing\.))?xxxxx.net\/api\/v\d+\/(.*)?\/play\/init.*/,
  new StaleWhileRevalidate({
    cacheName: 'play_init_cache',
    plugins: [{ cacheKeyWillBeUsed},
      new CacheableResponsePlugin({statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 10000,
        maxAgeSeconds: 7 * 24 * 60 * 60,
        purgeOnQuotaError: true,
      }),
    ],
  }),
);

示例图

图 1

图 2

相干材料

  1. 前端性能优化 – 缓存
  2. 【MDN】IndexedDB 浏览器存储限度和清理规范
  3. 网易云课堂 Service Worker 使用与实际
  4. workBox 官网
  5. 应用 Workbox
  6. workbox 缓存罕用范例
  7. workbox 路由申请
  8. 淘宝前端 Workbox 利用
  9. 神奇的 Workbox 3.0
  10. 饿了么的 PWA 降级实际
  11. 百度 Web 生态团队《PWA 利用实战》
  12. 深入浅出 PWA
  13. workbox-webpack-plugin 插件相干
退出移动版