README.md
Astro-service-worker
âď¸ Offline-capable Astro apps via SWSR (Service Worker Side Rendering)
astro-service-worker
will take your Astro SSR project, and create a service worker build out of it. This has several benefits:
- Your app is now offline-capable
- Your app is now installable
- The function invocations of your hosting provider are reduced dramatically, because requests can be served by the service worker in-browser
- Huge performance benefits
- Itâs a progressive enhancement
All you have to do is add the integration, and consider that the code you write in your Astro frontmatter will now also need to run in the browser/service-worker. This means that you will not be able to make use of Nodejs built-in dependencies, or other commonjs libraries. If you still want to write server-only code, you can use the networkOnly
configuration option.
Usage
serviceWorker
Install:
npm i -S astro-service-worker
Add the integration to your configuration:
astro.config.mjs
:
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
import serviceWorker from 'astro-service-worker';
export default defineConfig({
adapter: netlify(),
integrations: [
/** Creates a client-side service worker */
serviceWorker()
]
});
Note:
astro-service-worker
requires your app to run in SSR mode, instead of SSG mode.
worker
This package also includes an adapter to build your apps for worker-like environments, such as cloudflare.
astro.config.mjs
:
import { defineConfig } from 'astro/config';
import worker from 'astro-service-worker/adapter';
export default defineConfig({
/** Creates an integration for worker-like environments */
adapter: worker()
});
Configuration
serviceWorker
import serviceWorker from 'astro-service-worker';
export default defineConfig({
integrations: [
serviceWorker({
/** Provide custom service worker logic */
swSrc: 'user-sw.js',
/**
* Excludes specific pages from the service worker bundle, and forces them to always go to the network
* This is useful for server-only specific code, for example database connections
*/
networkOnly: ['/networkonly-astro'],
/** Configure workbox options */
workbox: {},
/** Both default to true, useful if you want to provide a custom installation experience */
skipWaiting: false,
clientsClaim: false,
/** Configure esbuild options */
esbuild: {},
/** Enables minifcation for esbuild, defaults to true */
minify: false,
/** Override the default service worker registration and update script */
swScript: '',
}),
]
});
worker
import worker, { cloudflare } from 'astro-service-worker/adapter';
export default defineConfig({
/** Using a preset: */
adapter: worker(cloudflare),
/** Configuration: */
adapter: worker({
/** Provide a module specifier to a custom shim file */
shim: [
/** local module */
`${process.cwd()}/custom-shim.js`,
/** bare module specifier */
'@worker-tools/location-polyfill'
],
})
});
Advanced configuration
serviceWorker
: Overwriting Workbox options
Internally, astro-service-worker
makes use of Workboxâs injectManifest
functionality. You can overwrite the default configuration via the workbox
options:
export default defineConfig({
integrations: [
serviceWorker({
workbox: {
globPatterns: ['**/*.{js,css,html,png,jpeg}'],
}
}),
]
});
serviceWorker
: Adding custom Service Worker logic
It could be the case that you need to extend the Service Worker to add custom logic. To do this, you can use the swSrc
option.
export default defineConfig({
integrations: [
serviceWorker({
swSrc: 'my-custom-sw.js',
}),
]
});
my-project/my-custom-sw.js
:
self.addEventListener('activate', (e) => {
console.log('Custom logic!');
});
Note that if you want to add custom logic for the
'fetch'
handler, you should use a middleware instead.
Note that you can also use modules in your custom service worker logic:
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
registerRoute(
/^https:\/\/fonts\.googleapis\.com/,
new StaleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
})
);
serviceWorker
: Combine with other integrations
You can also combine this integration with other integrations.
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
import customElements from 'custom-elements-ssr/astro.js';
import serviceWorker from 'astro-service-worker';
export default defineConfig({
adapter: netlify(),
integrations: [
customElements(),
serviceWorker()
]
});
serviceWorker
: Network-only
It could be the case that you would like to make use of some server-only endpoints or pages, perhaps for creating database connections, or other things that depend on Nodejs built-in modules that are not available in the browser. If that is the case, you can specify which page youâd like to exclude from the service worker bundle:
export default defineConfig({
integrations: [
serviceWorker({
networkOnly: ['/networkonly-page', '/db-endpoint', 'etc']
}),
]
});
worker
: Shim
It could be the case that other integrations will need to shim certain APIâs in the service worker, however. In this case, you can provide a custom import. The imports you provide here will be put at the very top of the service worker module before bundling.
import { defineConfig } from 'astro/config';
import worker from 'astro-service-worker/adapter';
export default defineConfig({
adapter: worker({
shim: [
// local module
`${process.cwd()}/custom-shim.js`,
// bare module specifier
'@worker-tools/location-polyfill'
]
}),
});
worker
: Presets
The adapter also comes with some environment specific presets, for example if youâre deploying on cloudflare, youâll want to use the cloudflare
preset:
import worker, { cloudflare } from 'astro-service-worker/adapter';
export default defineConfig({
adapter: worker(cloudflare)
});
If no wrangler.toml
is present, one will be created for you. If a wrangler.toml
is already present, youâre in charge of adding the required settings, hereâs an example:
name = "cloudflare-astro" # Name of your project
main = "dist/worker/index.js" # Path to your function
[site]
bucket = './dist' # Path to where your static assets are located
worker
|serviceWorker
: Middleware
Itâs also possible to add custom middleware to your service worker. To do so, you can add a function to self.MIDDLEWARE
. A middleware function gets passed the event
as well as Astroâs SSR manifest
, e.g.: middleware(event, manifest)
.
If a middleware returns a response, other middleware will no longer run, and event.respondWith
will be called with the response from the middleware that returned.
By default, Astro is the first middleware in the MIDDLEWARE
array. You can add any additional middleware to run after Astro. If you need to run code before Astro, you should prepend your middleware function to the self.MIDDLEWARE
array, instead of push
ing it to the end of the array. If no middleware has returned a response, the request will be sent to the network instead or when ran on the server, return a 404.
serviceWorker
:
For client-side service workers, you can configure this via the swSrc
property:
serviceWorker({swSrc: 'custom-handler.js'})
Where /custom-handler.js
:
self.MIDDLEWARE.push((event, manifest) => {
const url = new URL(event.request.url);
if (url.pathname.endsWith('.jpg')) {
return caches.match(event.request);
}
});
worker
:
If youâre creating a preset for a server-run worker-like environment, you can do this in a shim file, for example:
worker({shim: [`${process.cwd()}/my-shim.js`]})
Where /my-shim.js
:
import { getAssetFromKV } from '@cloudflare/kv-asset-handler';
self.MIDDLEWARE.push((event, manifest) => {
const url = new URL(event.request.url);
if(manifest.assets.has(url.pathname)) {
return getAssetFromKV(event);
}
});
Future: Streaming astro apps
In the future, once Astro release streaming responses, we can make use of that to improve performance even further:
/blog/[id].astro
:
---
import Header from '../src/components/Header.astro';
import Sidemenu from '../src/components/Sidemenu.astro';
import Footer from '../src/components/Footer.astro';
const { id } = Astro.params;
---
<html>
<Header/>
<Sidemenu/>
{fetch(`/blog/${id}.html`).then(render)}
<Footer/>
</html>
In a similar fashion to this Workbox example:
import { strategy } from 'workbox-streams';
import { registerRoute } from 'workbox-core';
const streamResponse = strategy([
() => caches.match(HEADER_CACHE_KEY, {cacheName: CACHE_NAME}),
() => `<nav>sidebar<ul><li><a href="/about">about</a></li></ul></nav>`,
({event}) => apiStrategy.handle({
event: event,
request: new Request('/content/foo.md'),
}),
() => caches.match(FOOTER_CACHE_KEY, {cacheName: CACHE_NAME}),
]);
registerRoute('/foo', streamResponse);
This is awesome because it means that you can now get the document starting to request your (SW cached) CSS, JS, and other âheaderâ resources in parallel with SW startup and the network fetch. None of the steps serialise until content comes back.
Given that the Astroâs render
function is a tagged template literal which returns an Astro
component, which is an async Iterable, it seems like this future may not be far off:
class AstroComponent {
constructor(htmlParts, expressions) {
this.htmlParts = htmlParts;
this.expressions = expressions;
}
get [Symbol.toStringTag]() {
return "AstroComponent";
}
*[Symbol.iterator]() {
const { htmlParts, expressions } = this;
for (let i = 0; i < htmlParts.length; i++) {
const html = htmlParts[i];
const expression = expressions[i];
yield markHTMLString(html);
yield _render(expression);
}
}
}
Extra links
Description
âď¸ Offline-first Astro apps via SWSR (Service Worker Side Rendering)
This was too much fun not to submit đ