diff --git a/package.json b/package.json index 08ca8ae67..552611cf3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "redux-logger": "^3.0.6", "redux-saga": "^1.1.3", "typescript": "~3.9.7", + "web-vitals": "^1.0.1", "xterm": "^4.6.0", "xterm-addon-fit": "^0.4.0", "zen-push": "^0.2.1" diff --git a/src/index.tsx b/src/index.tsx index 2cedface3..8844198b4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,7 +15,8 @@ import App from './components/App'; import NotificationStack from './components/NotificationStack'; import rootReducer from './reducers'; import rootSaga from './sagas'; -import * as serviceWorker from './serviceWorker'; +import * as serviceWorkerRegistration from './serviceWorkerRegistration'; +import reportWebVitals from './reportWebVitals'; const sagaMiddleware = createSagaMiddleware(); // TODO: add runtime option or filter - logger affects firmware flash performance @@ -59,8 +60,13 @@ ReactDOM.render( // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.register({ +// Learn more about service workers: https://cra.link/PWA +serviceWorkerRegistration.register({ onUpdate: (r) => store.dispatch(update(r)), onSuccess: (r) => store.dispatch(success(r)), }); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts new file mode 100644 index 000000000..ac2c28145 --- /dev/null +++ b/src/reportWebVitals.ts @@ -0,0 +1,15 @@ +import { ReportHandler } from 'web-vitals'; + +const reportWebVitals = (onPerfEntry?: ReportHandler): void => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/service-worker.ts b/src/service-worker.ts new file mode 100644 index 000000000..0419c7f25 --- /dev/null +++ b/src/service-worker.ts @@ -0,0 +1,80 @@ +/// +/* eslint-disable no-restricted-globals */ + +// This service worker can be customized! +// See https://developers.google.com/web/tools/workbox/modules +// for the list of available Workbox modules, or add any other +// code you'd like. +// You can also remove this file if you'd prefer not to use a +// service worker, and the Workbox build step will be skipped. + +import { clientsClaim } from 'workbox-core'; +import { ExpirationPlugin } from 'workbox-expiration'; +import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { StaleWhileRevalidate } from 'workbox-strategies'; + +declare const self: ServiceWorkerGlobalScope; + +clientsClaim(); + +// Precache all of the assets generated by your build process. +// Their URLs are injected into the manifest variable below. +// This variable must be present somewhere in your service worker file, +// even if you decide not to use precaching. See https://cra.link/PWA +precacheAndRoute(self.__WB_MANIFEST); + +// Set up App Shell-style routing, so that all navigation requests +// are fulfilled with your index.html shell. Learn more at +// https://developers.google.com/web/fundamentals/architecture/app-shell +const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); +registerRoute( + // Return false to exempt requests from being fulfilled by index.html. + ({ request, url }: { request: Request; url: URL }) => { + // If this isn't a navigation, skip. + if (request.mode !== 'navigate') { + return false; + } + + // If this is a URL that starts with /_, skip. + if (url.pathname.startsWith('/_')) { + return false; + } + + // If this looks like a URL for a resource, because it contains + // a file extension, skip. + if (url.pathname.match(fileExtensionRegexp)) { + return false; + } + + // Return true to signal that we want to use the handler. + return true; + }, + createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'), +); + +// An example runtime caching route for requests that aren't handled by the +// precache, in this case same-origin .png requests like those from in public/ +registerRoute( + // Add in any other file extensions or routing criteria as needed. + ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), + // Customize this strategy as needed, e.g., by changing to CacheFirst. + new StaleWhileRevalidate({ + cacheName: 'images', + plugins: [ + // Ensure that once this runtime cache reaches a maximum size the + // least-recently used images are removed. + new ExpirationPlugin({ maxEntries: 50 }), + ], + }), +); + +// This allows the web app to trigger skipWaiting via +// registration.waiting.postMessage({type: 'SKIP_WAITING'}) +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +}); + +// Any other custom service worker logic can go here. diff --git a/src/serviceWorker.ts b/src/serviceWorkerRegistration.ts similarity index 94% rename from src/serviceWorker.ts rename to src/serviceWorkerRegistration.ts index ff476e132..8de4e8e5c 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorkerRegistration.ts @@ -8,7 +8,7 @@ // resources are updated in the background. // To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA +// opt-in, read https://cra.link/PWA const isLocalhost = Boolean( window.location.hostname === 'localhost' || @@ -25,16 +25,50 @@ type Config = { onUpdate?: (registration: ServiceWorkerRegistration) => void; }; -function registerValidSW(swUrl: string, config?: Config): void { +export function register(config?: Config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://cra.link/PWA', + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl: string, config?: Config) { navigator.serviceWorker .register(swUrl) .then((registration) => { - registration.onupdatefound = (): void => { + registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker == null) { return; } - installingWorker.onstatechange = (): void => { + installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the updated precached content has been fetched, @@ -42,7 +76,7 @@ function registerValidSW(swUrl: string, config?: Config): void { // content until all client tabs are closed. console.log( 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', + 'tabs for this page are closed. See https://cra.link/PWA.', ); // Execute callback @@ -69,7 +103,7 @@ function registerValidSW(swUrl: string, config?: Config): void { }); } -function checkValidServiceWorker(swUrl: string, config?: Config): void { +function checkValidServiceWorker(swUrl: string, config?: Config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl, { headers: { 'Service-Worker': 'script' }, @@ -99,41 +133,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config): void { }); } -export function register(config?: Config): void { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA', - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } -} - -export function unregister(): void { +export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready .then((registration) => { diff --git a/yarn.lock b/yarn.lock index 8e25af422..b0d9152f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12259,6 +12259,11 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +web-vitals@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-1.0.1.tgz#025a7ffd9d8f5e4a030e3aee6061dcb38a4b6654" + integrity sha512-io/H/D18edTL1D2lcaUTLNLFEVZIPhNd4IdXDB9bEb+uDv2m/6NfyHiXKLFjbmI1ubeYpoQpR1gl9nlcWdI0vA== + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"