Skip to content

Service Worker

Weston Ruter edited this page Mar 4, 2022 · 14 revisions

As noted in a Google primer:

Rich offline experiences, periodic background syncs, push notifications—functionality that would normally require a native application—are coming to the web. Service workers provide the technical foundation that all these features rely on.

Only one service worker can be controlling a page at a time. This has prevented themes and plugins from each introducing their own service workers because only one wins. So the first step at adding support for service workers in core is to provide an API for themes and plugins to register scripts and then have them concatenated into a script that is installed as the service worker. There are two such concatenated service worker scripts that are made available: one for the frontend and one for the admin. The frontend service worker is installed under the home('/') scope and the admin service worker is installed under the admin_url('/') scope.

The API is implemented using the same interface as WordPress uses for registering scripts; in fact WP_Service_Worker_Scripts is a subclass of WP_Scripts. The instance of this class is accessible via wp_service_workers()->get_registry(). Instead of using wp_register_script() the service worker scripts are registered using wp_register_service_worker_script(). This function accepts two parameters:

  • $handle: The service worker script handle which can be used to mark the script as a dependency for other scripts.
  • $args: An array of additional service worker script arguments as $key => $value pairs:
    • $src: Required. The URL to the service worker on the local filesystem or a callback function which returns the script to include in the service worker.
    • $deps: An array of service worker script handles that a script depends on.

Note that there is no $ver (version) parameter because browsers do not cache service workers so there is no need to cache bust them.

Service worker scripts should be registered on the wp_front_service_worker and/or wp_admin_service_worker action hooks, depending on whether they should be active for the frontend service worker, the admin service worker, or both of them. The hooks are passed the WP_Service_Worker_Scripts instance, so you can optionally access its register() method directly, which wp_register_service_worker_script() is a simple wrapper of.

Here are some examples:

function register_foo_service_worker_script( $scripts ) {
	// $scripts->register() is the same as wp_register_service_worker_script().
	$scripts->register(
		'foo', // Handle.
		array(
			'src'  => plugin_dir_url( __FILE__ ) . 'foo.js', // Source.
			'deps' => array( 'app-shell' ), // Example dependency (see next registered below).
		)
	);

	// $scripts->register() is the same as wp_register_service_worker_script().
	$scripts->register(
		'app-shell', // Handle.
		array(
			'src'  => plugin_dir_url( __FILE__ ) . 'app-shell.js', // Source.
		)
	);
}
// Register for the frontend service worker.
add_action( 'wp_front_service_worker', 'register_foo_service_worker_script' );

function register_bar_service_worker_script( $scripts ) {
	$scripts->register(
		'bar',
		array(
			// Use a script render callback instead of a source file.
			'src'  => function() {
				return 'console.info( "Hello admin!" );';
			},
			'deps' => array(), // No dependencies (can also be omitted).
		)
	);
}
// Register for the admin service worker.
add_action( 'wp_admin_service_worker', 'register_bar_service_worker_script' );

function register_baz_service_worker_script( $scripts ) {
	$scripts->register( 'baz', array( 'src' => plugin_dir_url( __FILE__ ) . 'baz.js' ) );
}
// Register for both the frontend and admin service worker.
add_action( 'wp_front_service_worker', 'register_baz_service_worker_script' );
add_action( 'wp_admin_service_worker', 'register_baz_service_worker_script' );

See labeled GitHub issues and see WordPress core tracking ticket #36995.

Caching

Offline Browsing

In versions prior to 0.6, no caching strategies were added by default. The only service worker behavior was to serve an offline template when the client's connection is down or the site is down, and also to serve an error page when the server returns with 500 Internal Server Error. As of 0.6, there is a new “Offline browsing” toggle on the Reading Settings screen in the admin. It is disabled by default, but when enabled a network-first caching strategy is registered for navigations so that the offline page won't be shown when accessing previously-accessed pages. The network-first strategy is also used for assets from themes, plugins, and WordPress core. In addition, uploaded images get served with a stale-while-revalidate strategy.

Offline browsing toggle

When offline browsing is enabled, the route caching is enabled for:

  • Page navigations
  • Site assets (theme, plugin, core)
  • Uploaded images

Page Navigation Caching

Previously navigation requests used NetworkOnly strategy by default, which meant that pages accessed while navigating would not be cached at all, so they would not be available for offline browsing. Instead, the offline page would have been served instead, which is not super helpful. This PR changes the default to the NetworkFirst caching strategy for navigation requests, which will allow for previously-visited pages to continue to be accessed while offline, which is they key benefit of PWA.

The strategy is also now configured by default as follows:

  • cache_name: 'navigations'. This is the cache used for storing navigated pages. Note that the PWA plugin will automatically prefix the cache name to be specific to the WP site, especially for the sake of subdirectory installs. For example: wp-/-navigations.
  • network_timeout_seconds: 2 seconds. When accessing a page, it will first attempt to fetch the most recent version from origin. If that takes longer than 2 seconds, then the previously-cached page will be served This hopes that already-cached page assets will result in a page taking <500ms to render, thus leading to a good LCP measurement.
  • expiration.max_entries: 10. The 10 most recently-visited pages are cached.

The filter to customize these configurations is wp_service_worker_navigation_caching. Note that you can use snake_case format for the configuration options and the plugin will automatically convert it into camelCase for Workbox.js.

You can disable the caching of navigation responses by filtering to return an empty array like so:

add_filter( 'wp_service_worker_navigation_caching', '__return_empty_array' );

If the 2 second timeout is too small, resulting in cached pages being served too frequently, you can increase it with code like the following:

add_filter( 'wp_service_worker_navigation_caching', static function ( $config ) {
	$config['network_timeout_seconds'] = 5;
	return $config;
} );

Here's an example for how to use this filter to configure the service worker to:

  1. Use a StaleWhileRevalidate strategy for serving pages.
  2. Only hold onto those pages for 1 minute.
  3. Only cache responses which are OK and for non-authenticated users.
  4. Broadcast updates when pages are updated (so you can show a notification).

You can do so with the following plugin code:

// First, send a stale-while-revalidate Cache-Control header with pages for users that are not authenticated.
add_filter(
	'wp_headers',
	function ( $headers ) {
		if ( ! is_user_logged_in() && ! isset( $headers['Cache-Control'] ) ) {
			$headers['Cache-Control'] = 'max-age=1, stale-while-revalidate=59';
		}
		return $headers;
	}
);

// Now, configure the service worker to cache navigations to these pages.
add_filter(
	'wp_service_worker_navigation_caching',
	function ( $config ) {
		// Use the stale-while-revalidate strategy in the service worker.
		// Formerly specified via the wp_service_worker_navigation_caching_strategy filter.
		$config['strategy'] = WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE;

		// Formerly needing to have a parent 'plugins' key.
		$config['expiration'] = [
			// Only hold on to cached pages for 1 minute.
			// Formerly needing to be maxAgeSeconds.
			'max_age_seconds' => 60,
		];

		$config['cacheable_response'] = [
			// Only cache responses that are 200 OK.
			'statuses' => [ 200 ],
			// Only cache responses for unauthenticated users per wp_headers filter above.
			'headers'  => [
				'Cache-Control' => 'max-age=1, stale-while-revalidate=59', // See <https://web.dev/stale-while-revalidate/>.
			],
		];

		// Notify the page via postMessage when a stale page has been updated in the background.
		$config['broadcast_update'] = [];

		return $config;
	}
);

For more information on this filter and how it has evolved from previous versions of the plugin, see the relevant pull request.

Site Asset Caching

In addition to enabling the NetworkFirst caching strategy by default for navigation requests, this PR also enables the NetworkFirst caching strategy for theme, plugin, and core (wp-includes) assets. This strategy is used as opposed to StaleWhileRevaliate for the sake of development and since serving stale assets can leave the page in bad state. In any case, assets should already have far-future expiration in browser cache anyway.

The expiration max_entries for each are informed by the 75th percentile on HTTP Archive, see https://github.com/GoogleChromeLabs/pwa-wp/issues/265#issuecomment-706612536:

  • For themes, maximum asset entries are capped at 34. This includes ~622KB.
  • For plugins, maximum asset entries are capped at 44. This includes ~449KB.
  • For core (wp-includes), maximum asset entries capped at 14. This includes ~178KB.

Three new filters are introduced for customizing the caching strategy configuration for these three categories:

  • wp_service_worker_theme_asset_caching
  • wp_service_worker_plugin_asset_caching
  • wp_service_worker_core_asset_caching

These filters are passed a configuration array that looks the same as the array passed to wp_service_worker_navigation_caching for navigation requests, with one addition: there is a route array key which includes a regular expression pattern to match the asset URL routes to be included in caching.

Uploaded Image Caching

Lastly, this PR adds a StaleWhileRevalidate caching strategy for uploaded images. This includes the files located in the site's uploads directory which have the file extensions returned by wp_get_ext_types()['image']. Cache expiration is currently set to 1 month, with limiting the number of cached images to 100 entries.

As with configuring caching strategies for navigations, core assets, theme assets, and plugin assets, the uploaded images can be configured with the wp_service_worker_uploaded_image_caching filter.

Route Caching API

In addition to the above filters which are enabled via offline browsing, you may register your own caching routes. Service Workers in the feature plugin are using Workbox to power a higher-level PHP abstraction for themes and plugins to indicate the routes and the caching strategies in a declarative way. Since only one handler can be used per one route then conflicts are also detected and reported in console when using debug mode.

The API abstraction allows registering routes for caching and urls for precaching using the following two functions:

  1. wp_register_service_worker_caching_route(): accepts the following two parameters:
  • $route: Route regular expression, without delimiters.
  • $args: An array of additional route arguments as $key => $value pairs:
    • $strategy: Required. Strategy, can be WP_Service_Worker_Caching_Routes::STRATEGY_NETWORK_FIRST, WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST, WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE, WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_ONLY, WP_Service_Worker_Caching_Routes::STRATEGY_NETWORK_ONLY.
    • For more information about the behavior of the strategies, please see Workbox Strategies.
    • ℹ️ Typically navigation requests and theme/plugin assets should use the network-first strategy; uploaded files may use the cache-first strategy. It is not currently recommended to use the stale-while-revalidate strategy for navigation requests because there is no mechanism in place yet to inform the user that an update is available (see #217).
    • $cache_name: Name to use for the cache.
    • $plugins: Array of plugins with configuration. The key of each plugin in the array must match the plugin's name. See https://developers.google.com/web/tools/workbox/guides/using-plugins#workbox_plugins.
  1. wp_register_service_worker_precaching_route(): accepts the following two parameters:
  • $url: URL to cache.
  • $args: An array of additional route arguments as $key => $value pairs:
    • $revision: Revision, optional.

Examples of using the API:

add_action( 'wp_front_service_worker', function( \WP_Service_Worker_Scripts $scripts ) {
	$scripts->caching_routes()->register(
		'/wp-content/.*\.(?:png|gif|jpg|jpeg|svg|webp)(\?.*)?$',
		array(
			'strategy'  => WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
			'cacheName' => 'images',
			'plugins'   => array(
				'expiration' => array(
					'maxEntries'    => 60,
					'maxAgeSeconds' => 60 * 60 * 24,
				),
			),
		)
	);
} );
add_action( 'wp_front_service_worker', function( \WP_Service_Worker_Scripts $scripts ) {
	$scripts->precaching_routes()->register(
		'https://example.com/wp-content/themes/my-theme/my-theme-image.png',
		array(
			'revision' => get_bloginfo( 'version' ),
		)
	);
} );

If you would like to opt-in to a caching strategy for navigation requests, you can do:

add_filter( 'wp_service_worker_navigation_caching_strategy', function() {
	return WP_Service_Worker_Caching_Routes::STRATEGY_NETWORK_FIRST;
} );

add_filter( 'wp_service_worker_navigation_caching_strategy_args', function( $args ) {
	$args['cacheName'] = 'pages';
	$args['plugins']['expiration']['maxEntries'] = 50;
	return $args;
} );

Please note that if you are using the stale-while-revalidate strategy, it is important that you set the cache entries to expire the same time as the logged-in user nonce. For example:

add_filter(
	'wp_service_worker_navigation_caching_strategy',
	function() {
		return WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE;
	}
);
add_filter(
	'wp_service_worker_navigation_caching_strategy_args',
	function( $args ) {
		$args['cacheName']                           = 'pages';
		$args['plugins']['expiration']['maxEntries'] = 20;

		/** This filter is documented in wp-includes/pluggable.php */
		$max_age_seconds = apply_filters( 'nonce_life', DAY_IN_SECONDS );
		$args['plugins']['expiration']['maxAgeSeconds'] = $max_age_seconds;

		return $args;
	}
);

As noted above, the stale-while-revalidate strategy is not currently recommended.

(If you previously added a wp_service_worker_navigation_preload filter to disable navigation preload, you should probably remove it. This was originally needed to work around an issue with ensuring the offline page would work when using a navigation caching strategy, but it is no longer needed and it should be removed improved performance. Disabling navigation preload is only relevant when you are developing an app shell.)

Offline / 500 error handling

The feature plugins offers improved offline experience by displaying a custom template when user is offline instead of the default message in browser. Same goes for 500 errors -- a template is displayed together with error details.

Themes can override the default template by using error.php, offline.php, and 500.php in you theme folder. error.php is a general template for both offline and 500 error pages and it is overridden by offline.php and 500.php if they exist. If you want to supply such an template via a plugin instead of via your theme, see the PWA Offline Template example.

Note that the templates should use wp_service_worker_error_message_placeholder() for displaying the offline / error messages. Additionally, on the 500 error template the details of the error can be displayed using the function wp_service_worker_error_details_template( $output ).

For development purposes the offline and 500 error templates are visible on the following URLs on your site:

  • https://your-site-name.com/?wp_error_template=offline;
  • https://your-site-name.com/?wp_error_template=500

Default value for $output is the following: <details id="error-details"><summary>' . esc_html__( 'More Details', 'pwa' ) . '</summary>{{{error_details_iframe}}}</details> where {{{error_details_iframe}}} will be replaced by the iframe.

In case of using the <iframe> within the template {{{iframe_src}}} and {{{iframe_srcdoc}}} are available as well.

For example this could be done:

wp_service_worker_error_details_template(
    '<details id="error-details"><summary>' . esc_html__( 'More Details', 'pwa' ) . '</summary><iframe style="width:100%" src="{{{iframe_src}}}" data-srcdoc="{{{iframe_srcdoc}}}"></iframe></details>'
);

Offline Commenting

Another feature improving the offline experience is Offline Commenting implemented leveraging Workbox Background Sync API.

In case of submitting a comment and being offline (failing to fetch) the request is added to a queue and once the browsers "thinks" the connectivity is back then Sync is triggered and all the commenting requests in the queue are replayed. This meas that the comment will be resubmitted once the connection is back.

Available actions and filters

Here is a list of all available actions and filters added by the feature plugin.

Filters

  • wp_service_worker_skip_waiting: Filters whether the service worker should update automatically when a new version is available.
    • Has one boolean argument which defaults to true.
  • wp_service_worker_clients_claim: Filters whether the service worker should use clientsClaim() after skipWaiting().
    • Has one boolean argument which defaults to false;
  • wp_service_worker_navigation_preload: Filters whether navigation preload is enabled. Has two arguments:
    • boolean which defaults to true;
    • $current_scope, either 1 (WP_Service_Workers::SCOPE_FRONT) or 2 (WP_Service_Workers::SCOPE_ADMIN);
  • wp_offline_error_precache_entry: Filters what is precached to serve as the offline error response on the frontend.
    • Has one parameter $entry which is an array:
      • $url URL to page that shows the offline error template.
      • $revision Revision for the template. This defaults to the template and stylesheet names, with their respective theme versions.
  • wp_server_error_precache_entry: Filters what is precached to serve as the internal server error response on the frontend.
    • Has one parameter $entry which is an array:
      • $url URL to page that shows the server error template.
      • $revision Revision for the template. This defaults to the template and stylesheet names, with their respective theme versions.
  • wp_service_worker_error_messages: Filters the offline error messages displayed on the offline template by default and in case of offline commenting.
    • Has one argument with array of messages:
      • $default The message to display on the default offline template;
      • $comment The message to display on the offline template in case of commenting;

Actions

  • wp_front_service_worker: Fires before serving the frontend service worker, when its scripts should be registered, caching routes established, and assets precached.
    • Has one argument $scripts WP_Service_Worker_Scripts Instance to register service worker behavior with.
  • wp_admin_service_worker: Fires before serving the wp-admin service worker, when its scripts should be registered, caching routes established, and assets precached.
    • Has one argument $scripts WP_Service_Worker_Scripts Instance to register service worker behavior with.
  • wp_default_service_workers: Fires when the WP_Service_Worker_Scripts instance is initialized.
    • Has one argument $scripts WP_Service_Worker_Scripts Instance to register service worker behavior with.

Debugging

To enable verbose Workbox.js logging in the browser console, add the following constant to your wp-config.php:

define( 'WP_SERVICE_WORKER_DEBUG_LOG', true );

Integrations (DEPRECATED)

As of v0.7, the integrations are deprecated and using them will emit a warning in the browser console when the service worker is installed.

The plugin bundles several experimental integrations that are kept separate from the service worker core code. These integrations act as examples and proof-of-concept to achieve certain goals. While all of them are generally applicable and recommended to truly benefit from service workers, they are not crucial for the core API.

All these integrations are hidden behind a feature flag. To enable them, you can add service_worker theme support:

add_theme_support( 'service_worker', true );

Alternatively, you can selectively enable specific integrations by providing an array when adding theme support:

add_theme_support(
	'service_worker',
	array(
		'wp-site-icon'         => false,
		'wp-custom-logo'       => true,
		'wp-custom-background' => true,
		'wp-fonts'             => true,
	)
);

Most of the integrations involve precaching assets when the service worker is first installed. This ensures that all of the assets will be available when the user goes offline, but it has a couple downsides:

  1. Many more resources may be downloaded and cached than are actually needed. For example, the wp-custom-logo integration will precache all image sizes that are included in the srcset for the Custom Logo. When on a mobile device, this may mean downloading a large logo image size that is never used.
  2. Precached resources are served with a cache-first strategy. When the service worker is installed, each precached resource gets a corresponding revision. When a resource is changed, this necessitates a service worker update to download the new resource before it will be served. This will mean that the user may have to reload the page (after first closing other windows for that site) to see this update. This is particularly annoying during development, but it also makes

For these reasons, precaching resources should be done very sparingly. (The only two resources which are always precached are the offline page and the error page.) Instead, runtime caching should be used with a network-first caching strategy so that the latest version of a resource will be used whenever it is changed and then fallback to a previously-cached resource.

⚠️ The following integrations are experimental and not necessarily recommended and they may be deprecated/removed!

The available integrations are as follows:

  • wp-site-icon: Add the various site icon sizes to the precache.
  • wp-custom-logo: Add the full size custom logo image(s) to the precache.
  • wp-custom-header: Add the theme's custom header image(s) to the precache.
  • wp-custom-background: Add the theme's custom background image to the precache.
  • wp-scripts: Add scripts with the precache data flag set to the precache. Also included is the emoji script. The precache flag is added like so: wp_script_add_data( $handle, 'precache', true ).
  • wp-styles: Add styles with the precache data flag set to the precache. The precache flag is added like so: wp_style_add_data( $handle, 'precache', true ).
  • wp-fonts: Add assets used by Google Fonts to the runtime cache.
  • wp-admin-assets: Add all scripts, styles, and fonts used in the admin to the precache. This only applies to the service worker used in the admin.