Service Workers are a new technology in modern web browsers. They augment the normal web deployment model and empower applications to deliver reliability and performance on par with natively installed code.
Service Workers have many different capabilities, but the most important is their function as a network proxy. They intercept all outgoing HTTP requests made by the application and can choose how to respond to them. For example, they can query a local cache and deliver a cached response if one is available. Proxying isn't limited to requests made through programmatic APIs (such as fetch
), but includes resources referenced in HTML and even the initial request to index.html
itself. Service Worker-based caching is thus completely programmable, and doesn't rely on server-specified caching headers.
Unlike the other scripts which make up an application (such as the Angular app bundle), the Service Worker is preserved after the user closes the tab. The next time the application is loaded in that browser, the Service Worker is loaded first, and can intercept every request for resources to load the app. If the Service Worker is designed to do so, it can completely satisfy the loading of the app, without the need for the network.
Reducing dependency on the network by loading the application without it can significantly improve the user experience. Even across a fast reliable network, round-trip delays can introduce significant latency when loading the app. And in the real world, many factors can conspire to make networks unreliable.
Angular applications, as single-page applications, are in a prime position to benefit from the advantages of Service Workers. Starting with version 5.0.0, Angular ships with a Service Worker implementation. Angular developers can take advantage of this Service Worker and benefit from the increased reliability and performance it provides, without needing to code against low-level APIs themselves.
Angular's Service Worker is designed to optimize the end user experience of using an application over a slow or flaky connection while minimizing the risks of serving outdated content. Experience goals are as follows:
- Caching an application is like installing a native app. It should be cached as one unit, and all files should update together.
- An application running in a tab should not suddenly start receiving cached files from a newer version (which are likely incompatible).
- When users refresh the app, they should see the latest fully cached version. New tabs should also load the latest cached code.
- Updates should happen in the background, relatively quickly after changes are published. It's okay to serve the previous version of the app until an update is installed and ready.
- Bandwidth is limited, and the Service Worker should conserve it when possible. Resources should only be downloaded if they've changed.
To achieve these goals, Angular's Service Worker loads a "manifest" file from the server, which describes the resources to cache and includes hashes of every file's contents. When an update to the app is deployed, the contents of the manifest will change, letting the Service Worker know that a new version of the app should be downloaded and cached. This manifest is generated from a user-provided configuration file ngsw.json
, via a build tool such as the Angular CLI.
Installing Angular's service worker is as simple as including an NgModule
. In addition to registering the Angular Service Worker with the browser, this also makes a few services available for injection which interact with the Service Worker and can be used to control it. For example, an application can ask to be notified when a new update becomes available, or ask the worker to check the server for available updates.
Beginning in Angular x.x.x, you can easily enable Angular service worker support in any CLI project. This section explains how to anable Angular service worker support in new and existing projects. It then uses a simple example to show you a service worker in action, demonstrating loading and basic caching.
QUESTION: Should we state the minimal Angular version somewhere? QUESTION: Service Worker or service worker QUESTION: a service worker, the service worker, service worker support (I get the sense that there's a careful dance here because the sw isn't actually created until build time?)
If you're generating a new CLI project, you can instruct the CLI to set up the Angular service worker as part of the project creation by adding the --service-worker flag.
ng new --service-worker
QUESTION: To verify that the service worker was enabled....HOW???
QUESTION: Does a developer enable service worker support, set up a service worker, set up a project to use or include a service worker?
To add a service worker to an existing application:
- Install the service worker package.
- Enable service worker build support in the CLI.
- Create an ngsw.json configuration file. This file specifies QUESTION FILL THIS IN
- Add
ServiceWorkerModule
to yourAppModule
and use it to register the service worker.
These steps are explained in more detail below, in the context of an existing example application (AR: insert link to existing app).
First, install the package @angular/service-worker.
yarn install @angular/service-worker
The CLI needs to be told that this project requires generation of an Angular Service Worker manifest. This is done by setting a flag in .angular-cli.json
:
ng set apps.0.serviceWorker=true
Open src/app/app.module.ts
in an editor. This is the application's root module. Here is where the ServiceWorkerModule
should be included, using its .register()
helper to take care of registering the service worker for you.
First, add the import at the top of the file.
import {ServiceWorkerModule} from '@angular/service-worker';
import {environment} from '../environments/environment';
Next, add ServiceWorkerModule
to the @NgModule
imports section. It's better to do this with a conditional so that the Service Worker is only registered for a production application.
@NgModule({
...
imports: [
BrowserModule,
...
environments.production ? ServiceWorkerModule.register('ngsw-worker.js') : [],
})
export class AppModule {}
The file ngsw-worker.js
is the name of the prebuilt service worker script which the CLI will copy into dist/
to deploy along with your server.
Finally, the Angular CLI needs an ngsw.json
configuration file, which controls how files will be cached by the Service Worker. You could hand-write this file, but it's easier to use the boilerplate version from the CLI, which configures sensible defaults for most apps.
Save the following as src/ngsw.json
:
{
"index": "/index.html",
"assetGroups": [{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html"
],
"versionedFiles": [
"/*.bundle.css",
"/*.bundle.js",
"/*.chunk.js"
]
}
}, {
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**"
]
}
}]
}
$ ng build --prod
That's it, this CLI project is now set up to use Service Workers.
This section demonstrates a service worker in action, using an example application.
After the project is built, it's time to serve it. ng serve
does not work with service workers, you must use a real HTTP server to test your project locally. Apart from the build support, it's also a good idea to test on a dedicated port and to use an Incognito/Private window in your browser, to ensure the SW doesn't end up reading from leftover state, which can cause unexpected behavior.
$ cd dist
$ http-server -p 8080
With the server running, you can point your browser at http://localhost:8080/. Your application should load normally.
Next, open up the Developer Tools and go to the Network tab. Check the Offline checkbox.
This disables network interaction for your application. For non-Service Worker apps, refreshing now would trigger Chrome's Offline page (and you could jump a dinosaur over some cactii). This application, however, now has a service worker, so on a refresh the page loads normally. Go ahead, try it!
If you look at the Network tab after you do, you'll notice something interesting:
Under "Size", now the requests state (from ServiceWorker)
. This means that they're not being loaded from the network, but are instead loaded from the SW's cache.
Notice that all of the files needed to render this application are cached. The ngsw.json
boilerplate configuration earlier is set up to cache the specific resources used by the CLI:
- index.html
- favicon.ico
- Build artifacts (JS & CSS bundles)
- Anything under
assets
Now that you've seen how service workers cache your application, let's see how updates work in the wild. If you're testing in an incognito window, open a second blank tab. This will keep the incognito and the cache state alive during your test.
Next, close the application tab (but not the window). This should also close the Developer Tools. At the same time, shut down http-server
.
We're going to make a change to the application, and watch the Service Worker install the update.
Edit src/app/app.component.html
, and change something in the template. For example, change the text Welcome to {{title}}!
to Bienvenue a {{title}}!
.
Build and run your server again:
$ ng build --prod
$ cd dist
$ http-server -p 8080
Open http://localhost:8080 again in the same window. What happens?
What went wrong? Nothing, actually. The Angular Service Worker is doing its job and serving the version of the application that it has installed, even though there is an update available. The SW doesn't wait to check for updates before it serves the app that it has cached, because that would be slow.
If you look at http-server
's logs, you might catch the SW requesting /ngsw.json
. This is how the SW checks for updates.
Now, refresh the page with ctrl+r.
The service worker installed the updated version of your app in the background, and the next time the page was loaded (or reloaded), the SW switched to the latest version.
Importing ServiceWorkerModule
into your AppModule
doesn't just register the service worker, it also provides a few services you can use to interact with it and control the caching of your application.
The most commonly used of these services is SwUpdate
. This service gives you access to events that indicate when the SW has discovered an available update for your application, or has activated such an update (meaning it is now serving content from that update to your app).
It supports 4 separate operations:
- Getting notified of available updates. These are new versions of the app which will be loaded if the page is refreshed.
- Getting notified of update activation. This is when the SW starts serving a new version of the app immediately.
- Asking the SW to check the server for new updates.
- Asking the SW to activate the latest version of the application for the current tab.
The two update events are Observable
properties of SwUpdate
:
export class MyService {
constructor(updates: SwUpdates) {
updates.available.subscribe(event => {
console.log('current version is', event.current);
console.log('available version is', event.available);
});
updates.activated.subscribe(event => {
console.log('old version was', event.previous);
console.log('new version is', event.current);
});
}
}
You might use these events to notify the user of a pending update or to refresh their pages when the code they are running is out of date.
It's possible to ask the SW to check if any updates have been deployed to the server. You might choose to do this if you have a frequently-changing site or want updates to happen on a schedule.
This is done with the SwUpdate.checkForUpdate()
method:
class MyService {
constructor(updates: SwUpdates) {
// Check for updates every 6 hours.
Observable.interval(6 * 60 * 60).subscribe(() => updates.checkForUpdate());
}
}
This method returns a Promise
which indicates that the update check has completed successfully (though it does not indicate whether an update was discovered as a result of the check). Even if one is found, the SW must still successfully download the changed files, which can fail. If successful, the SwUpdates.available
event will indicate availability of a new version of the application.
In the event the current tab needs to be immediately updated to the latest application version, it can ask to do so with the SwUpdate.activateUpdate()
method:
class MyService {
constructor(updates: SwUpdates) {
updates.available.subscribe(event => {
if (promptUser(event)) {
updates.activateUpdate().then(() => window.reload());
}
});
}
}
Doing this could break lazy-loading into currently running applications, especially if the lazy-loaded chunks use filenames with hashes, which change every version.
Unlike the others, this section is not aimed at application developers, but rather the engineers responsible for deploying and supporting Angular Service Worker enabled applications in production. It details how NGSW fits into the larger production environment, its behavior under various conditions, and available recourses and failsafe if the SW's behavior becomes problematic.
Conceptually, you can imagine NGSW as a forward cache or a CDN edge which is installed in the end user's browser. Its job is to satisfy requests for resources or data from a local cache, without needing to wait for the network. Like any cache, it has rules for how content is expired and updated.
NGSW has the concept of a "version" - a collection of resources representing a specific build of the application. Whenever a new build of the application is deployed (even if only a single file was updated), NGSW treats it as a new version. At any given time, NGSW may have multiple versions of the application in its cache, and may be serving them simultaneously (see the "Tabs" section below).
It's important that NGSW group all files into a version together. HTML, JS, and CSS files frequently refer to each other and make assumptions about specific contents. For example, an /index.html
file may have a <script>
tag referencing /bundle.js
and attempt to call a function startApp()
from within that script. Any time this version of the index file is served, the corresponding bundle must be served with it. If both files are later updated, it's possible that the startApp()
function may have been renamed to runApp()
. It's not valid to serve the old index (which calls startApp()
) along with the new bundle (which defines runApp()
).
In Angular applications, this is especially important due to lazy loading. A JS bundle may reference many lazy chunks, and the filenames of the lazy chunks are unique to the particular build of the application. If a running application at version X attempts to load a lazy chunk, but the server has updated to version X + 1 already, the lazy loading operation will fail.
Thus, the version identifier of the application is determined by the contents of all resources, and changes if any of them change. In practice, this version is determined by the contents of the ngsw.json
file. This file includes hashes for all known content. If any of these files change, its hash will change in ngsw.json
, causing NGSW to treat the active set of files as a new version.
In so doing, NGSW can ensure that an Angular application always sees a consistent set of files.
NGSW detects when an update to the application is available by looking for updates to the ngsw.json
manifest. This update check happens every time the Service Worker is started. Typically this occurs randomly throughout the usage of the application - the browser will terminate the service worker if the page is idle beyond a given timeout.
One of the dangers of long caching is inadvertently caching an invalid resource. In a normal HTTP cache, a hard refresh or cache expiration will limit the negative effects of caching an invalid file. A Service Worker ignores such constraints, though, and effectively long caches the entire application. It is critical, then, that the Service Worker gets the correct content.
To ensure this, the Angular Service Worker validates the hashes of all resources for which it has them. Typically for a CLI application, this is everything in the dist
directory covered by the user's src/ngsw.json
configuration.
If a particular file fails validation, NGSW will first attempt to re-fetch the content using a "cache-busting" URL parameter to eliminate the effects of browser or intermediate caching. If that content also fails validation, NGSW will consider the entire version of the application to be invalid, and stop serving it. If necessary, it will enter a safe mode where requests fall back on the network, opting not to utilize its cache if the risk of serving invalid/broken content is high.
Hash mismatches can occur for a variety of reasons:
- Caching layers in between the origin server and the end user could serve stale content.
- A non-atomic deployment could result in NGSW having visibility of partially updated content.
- Errors during the build process could result in updated resources without
ngsw.json
being updated, or visca versa.
The only resources which have hashes in the ngsw.json
manifest are those which were present in the dist
directory at the time the manifest was built. Other resources, especially those loaded from CDNs, have content which is unknown at build time or updates more frequently than the application is deployed.
If NGSW does not have a hash to validate a given resource, it will still cache its contents, but it will honor the HTTP caching headers using a policy of "stale while revalidate." That is, when HTTP caching headers for a cached resource indicate that it has expired, NGSW will continue to serve the content, but will attempt to refresh the resource in the background. This way, broken unhashed resources do not get stuck in the cache beyond their configured lifetimes.
It can be problematic for an application if the version of resources it's receiving changes suddenly or without warning (see the "Versions" section above for a description of such issues). NGSW provides a guarantee - a running application will always see the same version of the application. This means that in practice, different open tabs can be running different versions of the app.
It's important to note that this guarantee is stronger than that provided by the normal web deployment model. Without a Service Worker, there is no guarantee that code lazily loaded later in a running application is from the same version as the initial code for the app.
There are a few different reasons why NGSW might change the version of a running application. Some of them are error conditions:
- The current version becomes invalid due to a failed hash.
- An unrelated error causes the Service Worker to enter safe mode (temporary deactivation).
NGSW is aware of which versions are in use at any given moment, and will clean up versions once no tab is using them.
Others are normal events:
- The page is reloaded/refreshed.
- The page requests an update be immediately activated via the
SwUpdate
service.
The Angular Service Worker is itself a small script which runs in end user browsers. From time to time, it will be updated with bug fixes and feature improvements.
Service Workers have their own update process. Occasionally (usually when the application is first opened or first accessed in a while), the SW script will be downloaded. If it has changed, the worker will be updated in the background.
Most updates to the Angular Service Worker will be transparent to the application. The old caches will still be valid, and content will be served normally. However, occasionally a bugfix or feature in NGSW will require the invalidation of old caches. In this case, the application will be refreshed from the network, transparently.
Occasionally it may be necesssary to examine Angular Service Worker in a running state, to investigate issues or to ensure that it is operating as designed. Browsers provide built-in tools for debugging Service Workers, and NGSW itself includes useful debugging features.
NGSW exposes debugging information under the /ngsw/
virtual directory. Currently the single exposed URL is /ngsw/state
. Here is an example of this debug page's contents:
NGSW Debug Info:
Driver state: NORMAL ((nominal))
Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c
Last update check: never
=== Version eea7f5f464f90789b621170af5a569d6be077e5c ===
Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65
=== Idle Task Queue ===
Last update tick: 1s496u
Last update run: never
Task queue:
* init post-load (update, cleanup)
Debug log:
There's a lot of useful information here. Piece by piece:
Driver state: NORMAL ((nominal))
The SW is operating normally, and is not in a degraded state. There are also two possible degraded states:
EXISTING_CLIENTS_ONLY
: the SW does not have a clean copy of the latest known version of the app. Older cached versions are safe to use, so existing tabs will continue to run from cache, but new loads of the application will be served from network.SAFE_MODE
: the SW cannot guarantee the safety of using cached data. Either an unexpected error occurred or all cached versions are invalid. All traffic will be served from the network, running as little SW code as possible.
In both cases, the parenthetical annotation will detail the error that caused the SW to enter the degraded state.
Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c
This is the SHA1 hash of the most up to date version of the application that the SW knows about.
Last update check: never
The SW has never checked for a new version of the app. In this case, the update check is currently scheduled (as you will see below).
=== Version eea7f5f464f90789b621170af5a569d6be077e5c ===
Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65
This SW has one version of the application cached, being used to serve two different tabs. Note that this version hash is the "latest manifest hash" listed above - both clients are on the latest version. Each client is listed by its ID from the Clients
API in the browser.
=== Idle Task Queue ===
Last update tick: 1s496u
Last update run: never
Task queue:
* init post-load (update, cleanup)
The Idle Task Queue is the queue of all pending tasks which happen in the background in the Service Worker. If there are any tasks in the queue, they'll be listed with a description. In this case, the SW has one such task scheduled, a post-initialization operation involving an update check and cleanup of stale caches.
The last update tick/run counters give the time since specific events happened related to the idle queue. The "Last update run" counter shows the last time idle tasks were actually executed. "Last update tick" shows the time since the last event after which the queue might be processed.
Debug log:
Errors that occur within the SW will be logged here.
Browsers like Chrome provide strong Developer Tools for interacting with Service Workers. Such tools can be powerful when used properly, but there are a few caveats.
- When using Dev Tools, the Service Worker is kept running in the background and never restarts. For NGSW, this means that update checks to the application will generally not happen.
- If looking in the Cache Storage viewer, it's frequently out of date. Right clicking the Cache Storage title and refreshing the caches is essential.
Stopping and starting the Service Worker in the Service Worker pane will trigger a check for update.
Like any complex system, bugs or broken configurations can cause NGSW to act in unforeseen ways. While its design attempts to minimize the impact of such problems, NGSW contains a failsafe mechanism in case an administrator ever needs to deactivate the Service Worker quickly.
It's very easy to activate: if the service worker's request for ngsw.json
returns a 404
, then the Service Worker will remove all of its caches and unregister itself, essentially self-destructing.
The src/ngsw.json
config file describes which files and data URLs the Angular Service Worker should cache, and how they should be updated. When using the CLI, it's processed during ng build --prod
. Manually, it can be processed with the ngsw-config
tool:
$ ngsw-config dist src/ngsw.json /base/href
The config file uses a JSON format. All file paths must begin with /
, which is the deployment directory (usually dist
in CLI projects).
Patterns use a limited glob format - **
matches 0 or more path segments, *
matches exactly one path segment or filename segment. A !
prefix marks the pattern as being negative - only files that don't match it will be included.
Example patterns:
/**/*.html
- all HTML files/*.html
- only HTML files in the root!/**/*.map
- exclude all sourcemaps
This section is completely open ended. A developer can pass any data they'd like that describes this particular version of the application. It's included in the update notifications sent via the SwUpdate
service. Many applications use this section to provide additional information useful for the display of UI popups notifying users of the available update.
The file to serve as the index page, to satisfy navigation requests. Usually /index.html
.
"Assets" are resources which are part of the application version, and update along with the application. They can include resources loaded from the page's origin, as well as third-party resources loaded from CDNs and other external URLs. Not all such external URLs may even be known at build time - URL patterns can be matched.
This field contains an array of asset groups, each of which defining a set of asset resources and the policy by which they'll be cached.
{
"assetGroups": [{
...
}, {
...
}]
}
Each asset group specifies both a group of resources and a policy which governs them. This policy determines when the resources are fetched and what happens when changes are detected.
Asset groups follow the following Typescript interface:
interface AssetGroup {
name: string;
installMode?: 'prefetch' | 'lazy';
updateMode?: 'prefetch' | 'lazy';
resources: {
files?: string[];
versionedFiles?: string[];
urls?: string[];
};
}
A name
is mandatory, and is used to identify this particular group of assets between versions of the configuration.
The installMode
determines how these resources are initially cached. An installMode
of prefetch
tells NGSW to fetch every single listed resource that it can as it's caching the current version of the application. This is bandwidth-intensive, but ensures resources are available whenever they're requested, even if the browser is currently offline.
An installMode
of lazy
does not cache any of the resources up front. Instead, NGSW will cache resources for which it receives requests (effectively, this is an on-demand caching mode). Resources which are never requested will not be cached. This is useful for things like images at different resolutions, so the SW only caches the correct assets for the particular screen and orientation.
Once a resource is in the cache, updateMode
determines caching behavior when a new version of the application is discovered. Any resources in the group that have changed since the previous version will be updated in accordance with updateMode
.
An updateMode
of prefetch
tells the Service Worker to download and cache the changed resources immediately. A mode of lazy
tells the Service Worker to not cache those resources, but treat them as unrequested, and wait until they're requested again before updating them. A mode of lazy
is only valid if the installMode
is also lazy
.
This section describes the resources to cache, broken up into 3 groups.
files
lists patterns that match files in the distribution directory. These can be single files or glob-like patterns (see above) which match a number of files.
versionedFiles
is like files
, but should be used for those build artifacts which already include a hash in the filename (used for cache busting). NGSW can optimize some aspects of its operation if it can assume file contents are immutable.
urls
includes both URLs and URL patterns which will be matched at runtime. These resources are not fetched directly and do not have content hashes, but will be cached according to their HTTP headers. This is most useful for CDNs such as the Google Fonts service.
Unlike asset resources, data requests are not versioned along with the application. They're cached according to manually configured policies that are more useful for things like API requests and other data dependencies.
Data groups follow this Typescript interface:
export interface DataGroup {
name: string;
urls: string[];
version?: number;
cacheConfig: {
maxSize: number;
maxAge: string;
timeout?: string;
strategy?: 'freshness' | 'performance';
};
}
Like with assetGroups
, every data group has a name
which uniquely identifies it.
A list of URL patterns. URLs that match these patterns will be cached according to this data group's policy.
Occasionally APIs change formats in a backwards-incompatible way. A new version of the application may not be compatible with the old API format, and thus may not be compatible with existing cached resources from that API.
version
provides a mechanism to indicate that the resources being cached have been updated in a backwards-incompatible way, and that the old cache entries (those from previous versions) should be thrown out. version
is an integer field which defaults to 0
.
This section defines the policy by which matching requests will be cached.
The maximum number of entries (responses) in the cache. This is a mandatory parameter. Open-ended caches can grow in unbounded ways and will eventually exceed storage quotas, calling for eviction.
Also a required parameter which indicates how long responses are allowed to remain in the cache before being considered invalid and evicted. maxAge
is a duration string, using the following unit suffixes:
d
- daysh
- hoursm
- minutess
- secondsu
- milliseconds
For example, the string 3d12h
will cache content for up to 3 and a half days.
This duration string specifies the network timeout - how long NGSW will wait for the network to respond before utilizing a cached response, if configured to do so.
NGSW has two major caching strategies it can use for data resources.
A strategy of performance
(the default) optimizes for responses that are as fast as possible. If a resource exists in the cache, the cached version will be used. This trades off some allowed staleness (depending on the maxAge
) for performance. This is suitable for resources which don't change often (e.g. user avatar images)
A strategy of freshness
attempts to minimize staleness by preferentially fetching requested data from the network. Only if the network times out (according to timeout
) does the request fall back to the cache. This is useful for resources which change frequently (e.g. user balances).