Progressive Web App tools
The App Platform provides some tools that can enable some PWA features and offline caching.
These features are considered experimental and are subject to breaking changes outside of the normal release cycle. They are for advanced purposes only and have some significant drawbacks in their first implementation. Keep an eye out for a more robust implementation in the future!
Opting In
You can opt in to PWA features using options in d2.config.js
. Here are the options and their effects:
config Property | Type | Description |
---|---|---|
pwa.enabled | Boolean | If true, enables registration of a service worker to perform offline caching in both development and production builds. This is required to enable Cacheable Sections. In development mode, the service worker uses different caching strategies to facilitate development; see below. If false or not set, any service worker registered in this scope will be unregistered. |
pwa.caching | Object | Contains several properties to configure offline caching by the service worker; see the definitions of the following properties below. |
pwa.caching.omitExternalRequestsFromAppShell | Boolean | If true, omits requests to external domains from the default app shell caching strategies. If false (default), requests to external domains will be cached in the app shell. Note that this setting does not affect the recording mode. |
pwa.caching.omitExternalRequests | Boolean | Deprecated; superceded by omitExternalRequestsFromAppShell . The new option takes precedence. |
pwa.caching.patternsToOmitFromAppShell | Array of RegExps or Strings | A list of URL patterns to omit from the default app shell caching strategies. Strings will be converted to RegExes using new RegExp(str) (with their special characters escaped) to test URLs. If a URL matches one of these patterns, that request will not be cached as part of the app shell. Note that this setting does not affect the recording mode. When choosing these URL filters, note that it is better to cache too many things than to risk not caching an important part of the app shell which could break the offline functionality of the app, so choose your filter patterns accordingly. |
pwa.caching.patternsToOmitFromCacheableSections | Array of RegExps or Strings | Similar to the above setting, except this is a list of URL patterns to omit from cacheable (recorded) sections. Requests with URLs that are filtered out from cacheable sections can still be cached in the app shell cache, unless they are filtered out from the app shell as well using the setting above. When choosing these URL filters, note that it is better to cache too many things than to risk not caching an important part of the section which could break the offline functionality of the section, so choose your filter patterns accordingly. |
pwa.caching.patternsToOmit | Array of RegExps or Strings | Deprecated; superceded by patternsToOmitFromAppShell . The new option takes precedence. |
pwa.caching.additionalManifestEntries | Array of Objects with signature { revision: String, url: String } | A list of files that can be added to the precache manifest. Note that the service worker uses Workbox to precache all static assets that end up in the ‘build’ folder after the CRA compilation and build step during the d2-app-scripts build process. The format of this list must match the required format for Workbox precache manifests, i.e. it must include a revision hash to inform when that file needs to be updated in the precache. |
pwa.caching.globsToOmitFromPrecache | An array of glob Strings | A list of globs that will cause matching files to be omitted from precaching. By default, all the contents of the build folder are added to the precache to give the app the best chances of functioning completely while offline. Developers may choose to omit some of these files (for example, thousands of font or image files) if they cause cache bloat and the app can work fine without them precached. |
The globs should be relative to the 'public' directory of the built app. For example, if you have a folder of fonts in your app in ./public/fonts/ that you want to omit from precaching, you can use the glob 'fonts/**' in this array. The omitted files can still be cached at runtime later. |
Offline caching
If PWA is enabled, a service worker will be registered in the scope of the app that will perform offline caching by default using a few strategies with the help of Workbox:
- Static assets that are part of the app’s
build
directory will be precached. After the app loads, the service worker will fetch all the static assets and add them to the Workbox precache, and they will be served cache-first from then on. Workbox makes sure these files are up-to-date by adding revision hashes to the files and updating files accordingly. For more details, see “Workbox Precaching”. - Other requests made by the app will be handled by default by two other strategies, and they both will be subject to a configurable URL filter (see
pwa.caching
options in the table above) to determine if a request should be cached or not.- Requests for static assets will be handled by a stale-while-revalidate strategy.
- Requests for data will be handled by a network-first strategy.
- To learn more about these strategies, see “Workbox Strategies”
Requests to URLs that are filtered out by the URL filtering options above will not be handled by the service worker, except during “recording mode” when all requests are handled.
In development
When using a service worker in a development environment (i.e. process.env.NODE_ENV === 'development'
, or when you're starting an app with yarn start
/d2-app-scripts start
), the caching strategies are different to facilitate development: anything that would be precached or handled by a stale-while-revalidate strategy is instead handled by a network-first strategy so that stale data or assets are never served, and it's easy to use newly compiled scripts that a development server is hosting. Everything that used the network-first strategy before, recording mode, and URL filtering all work the same in development mode.
Recording mode and “cacheable sections”
A special caching use case has been developed with the intention of supporting caching individual dashboards for offline. Here are some characateristics of cases where it’s useful:
- There are sections of an app you don’t want to cache by default, but do want to be able to cache when a user requests it. Consider the dashboards app: it is useful to save some dashboards for offline use, but it would be impractical to cache all of a user’s (potentiall many) dashboards by default.
- You want to track the caching status of those sections, i.e. if a section is cached or not and when it was last updated
To serve these use cases, Cacheable Sections and recording mode are included in the App Platform's offline features. A Cacheable Section can reload a component, then instruct the service worker to listen to all the relevant data requested to render that component and cache it. Then the app can be taken offline and that section will be viewable.
In order to not cache particular content until a user requests to do so using a Cacheable Section, you can use the patternsToOmit
option to omit requests to particular URLs from the default caching behavior; see the description in the d2.config.js
table above.
Cacheable Section React API
To make a section of an app available for offline use (that would otherwise not be cached by the default strategies, for example if the content is filtered out by URLs), like an individual dashboard in the Dashboards app, wrap that section in a <CacheableSection id={id}>
component and provide it an ID. To access the controls for that section and to trigger caching “recording,” use the useCacheableSection
hook keyed by that section’s ID. Read more about these features in the App Runtime documentation.
Service worker design
When “recording mode” is used and a “recording session” is triggered, the service worker listens to all network traffic and caches all the request/response pairs. In order to capture all the requests and responses for a section that triggers a cascade of requests as subcomponents of that section load and trigger more requests, the service worker will continue recording traffic after all pending requests have finished, up to a configurable timeout delay. If any new requests are initiated before the end of that delay, the delay will be canceled and will start again when all pending requests have finished again. This delay should be configured so that all the cascading requests make it in to the recorded section without prematurely stopping the recording and missing relevant requests.
As a measure to protect against saving faulty recordings, once the recording timeout has elapsed and the recording is moving to finish, the service worker will send a message to the client to confirm that this recording should be completed and saved. The client needs to send back a confirmation message within a 10 second timeout, or the recording will be scrapped. If the service worker receives confirmation, it will save the cached request/response pairs for that section and write some metadata about the recorded section in an IndexedDB.
Service worker messages
A number of messages are sent between the service worker and a client using the postMessage
interface to communicate and initiate different actions.
NB These are internal and added only for developer reference.
Message type (event.data.type) | Sent by? (SW or Client) | Payload expected (event.data.payload) | Event | Notes |
---|---|---|---|---|
SKIP_WAITING | Client | Client confirms using newly installed service worker | Activates newly installed and waiting service worker. A ‘controller change’ event will be triggered, upon which the client should reload the window to use the most recent static assets. | |
START_RECORDING | Client | { sectionId: String, recordingDelay: Int } | Starts recording mode | Recording delay is how long to wait (in ms) after all pending requests have finished before stopping recording |
RECORDING_STARTED | Service worker | Service worker is prepared to record network requests | Safe to initiate recorded network requests without triggering race conditions | |
RECORDING_ERROR | Service worker | { error: Error } | Error in recording | Something went wrong with the recording; any recorded requests are scrapped. Previous recordings are safe from being overwritten. |
CONFIRM_RECORDING_COMPLETION | Service worker | Recording has finished | As a measure to avoid saving faulty recordings, the service worker will prompt the client to confirm completion of the recording. A 10-second timer will be started, and if the client has not responded with a “CONFIRM_COMPLETION” message, the recording will be scrapped. | |
COMPLETE_RECORDING | Client | Responding to service worker that requests completion | Client confirms that conditions are good and the completed recording should be saved. A previous recording will be overwritten at this point, and data about the recording will be saved in the IndexedDB. | |
RECORDING_COMPLETED | Service worker | Recording completed & saved | The above completion operations are complete. The client can now resume normal activity. | |
DELETE_RECORDED_SECTION | Client | { sectionId: String } | Delete a section | If a recorded section with the specified ID exists, its cached requests will be deleted and the section data in IndexedDB will be removed. |