Skip to main content

Progressive Web App tools

The App Platform provides some tools that can enable some PWA features and offline caching.

warning

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 PropertyTypeDescription
pwa.enabledBooleanIf 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.cachingObjectContains several properties to configure offline caching by the service worker; see the definitions of the following properties below.
pwa.caching.omitExternalRequestsFromAppShellBooleanIf 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.omitExternalRequestsBooleanDeprecated; superceded by omitExternalRequestsFromAppShell. The new option takes precedence.
pwa.caching.patternsToOmitFromAppShellArray of RegExps or StringsA 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.patternsToOmitFromCacheableSectionsArray of RegExps or StringsSimilar 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.patternsToOmitArray of RegExps or StringsDeprecated; superceded by patternsToOmitFromAppShell. The new option takes precedence.
pwa.caching.additionalManifestEntriesArray 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.globsToOmitFromPrecacheAn array of glob StringsA 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:

  1. 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”.
  2. 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.
    1. Requests for static assets will be handled by a stale-while-revalidate strategy.
    2. Requests for data will be handled by a network-first strategy.
    3. 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)EventNotes
SKIP_WAITINGClientClient confirms using newly installed service workerActivates 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_RECORDINGClient{ sectionId: String, recordingDelay: Int }Starts recording modeRecording delay is how long to wait (in ms) after all pending requests have finished before stopping recording
RECORDING_STARTEDService workerService worker is prepared to record network requestsSafe to initiate recorded network requests without triggering race conditions
RECORDING_ERRORService worker{ error: Error }Error in recordingSomething went wrong with the recording; any recorded requests are scrapped. Previous recordings are safe from being overwritten.
CONFIRM_RECORDING_COMPLETIONService workerRecording has finishedAs 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_RECORDINGClientResponding to service worker that requests completionClient 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_COMPLETEDService workerRecording completed & savedThe above completion operations are complete. The client can now resume normal activity.
DELETE_RECORDED_SECTIONClient{ sectionId: String }Delete a sectionIf a recorded section with the specified ID exists, its cached requests will be deleted and the section data in IndexedDB will be removed.