Cloudflare worker running a Preact Progressive Web App
This example app deploys the awesome Progressive Web App created by preact-cli
to a Cloudflare worker. It also implements dynamic server side rendering using a Redux store. It uses the default Material Design preact-cli
template, with the addition of Redux.
The app is interactive in 1.2 seconds on mobile 3G as tested at webpagetest.org using Chrome on a Motorola G (gen 4) tested from Dulles, Virginia on a 1.6 Mbps 3G connection with 300ms of latency. Check out the results here.
View the demo at: https://growthcloud.io
js
and css
assets right in the worker provide consistent ultra-low latency serving of static assets. Even when the worker is 'cold' and time to first byte is slower, the server push of js
and css
is pretty consistent because they are coming out of the now running worker (with server push). We've noticed 40-50ms times for our bundles (on desktop) regardless if the worker was hot or cold.[email protected]
Feel free to open an issue with any comments, questions, or feedback! This is just a proof of concept to see that it could really be done. A lot still needs to be done to clean it up! If you're interested in collaborating on this, open an issue and let us know!
If you want to deploy your own PWA onto a Cloudflare worker, here are some basic instructions (also as a reminder to myself).
You will need to point your nameservers at Cloudflare's DNS. It seems this has to be the root domain, it's a shame we cannot delegate a subdomain to Cloudflare DNS. Once that is done it seems that you also need to create some root record for your domain within the Cloudflare DNS. I just pointed a CNAME at our S3 bucket. Maybe this is not needed, but it seemed to be.
You then need to enable workers for your domain. It costs $5.00 per month and that covers the first 5,000,000 requests.
It's pretty easy to use curl
to deploy the worker and it's very fast. Also it seems most of the time the live worker is updated almost immediately. Although sometimes it can take a little time to see the new version (very rarely more than 30 seconds).
There is a simple Makefile
in this repo with a couple commands to build and deploy. If you want to use it you'll need to get your Cloudflare keys and add them to a .env
file. The Cloudflare docs for deploying and finding your keys.
# save this to a .env file
ACCOUNT_AUTH_KEY=
ZONE_ID=
ACCOUNT_EMAIL=
STATIC_ASSETS_BASE_URL=we-use-an-s3-bucket-here
After that you can run make cf
to build and deploy everything. Have a look at the Makefile
if you want to see the commands.
We add redux in the index.js
file of the default app created by preact-cli
. For server side rendering we accept two props reduxStateFromServer
and url
.
The default index.js
import "./style";
import App from "./components/app";
export default App;
Adding Redux to index.js
. Note that Provider
comes from preact-redux
.
import "./style";
import App from "./components/app";
import store from "./store";
import { Provider } from "preact-redux";
export default ({ reduxStateFromServer, url }) => {
const initialState =
typeof window !== "undefined"
? // we are on the client let's rehydrate the state from the server, if available
window.__PRELOADED_STATE__ || {
// if all goes well, we should never see this state
name: "Default name from client side"
}
: // we are on the server
reduxStateFromServer;
return (
<Provider store={store(initialState)}>
<App url={url} />
</Provider>
);
};
Then for server side rendering we import the ssr bundle created by preact-cli
and pass it the needed props. See the whole file here.
import { h } from "preact"; // eslint-disable-line no-unused-vars
import render from "preact-render-to-string";
import App from "../build/ssr-build/ssr-bundle";
// ... //
const serverGatheredState = await (() =>
Promise.resolve({ name: "State rehydrated from the server" }))();
const html = render(
<App url={pathname} reduxStateFromServer={serverGatheredState} />
);
// ... //
preact.config.js
We utilize preact.config.js
to modify the default webpack
config inside the preact-cli
. We use the on-build-webpack
plugin to create the files needed for our worker so that we can embed and utilize server push for critical js
and css
assets.
We serve non-critical assets from an s3 bucket. This url can be set in the .env
file as STATIC_ASSETS_BASE_URL
and it will be included into the worker with dotenv-webpack
. See webpack.config.worker.js
. We've used s3, but it doesn't need to be s3 specifically.
This feels like a bit of a hack and a deeper customization of the service worker would be more appropriate, but we haven't done that yet.
In any case to get the Service Worker to cache multiple routes, we updated the preact-cli
build command in package.json
and added a prerender-urls.json
file.
The prerender-urls.json
file.
[{ url: "/" }, { url: "/profile" }];
Update package.json
{
"build": "preact build --prerender --prerenderUrls src/prerender-urls.json",
}
Note that this generates files at build/index.html
and build/profile/index.html
. We never use these generated files and when the Service Worker requests them we return a runtime generated response using our redux store and SSR. In this way we can provide a unique offline experience for each user depending on the data they load.
To complete this temporary hack we also create a little mapping in our worker to get SSR to return the correct pages.
const simpleRouterHack = (pathname, ssr) => {
return pathname === "/index.html" || pathname === "/"
? "/"
: pathname === "/profile" ||
pathname === "/profile/" ||
pathname === "/profile/index.html"
? "/profile"
: // a route like /profile/:username/extra-field should be a 404
pathname.includes("/profile") && !pathname.split("/")[3]
? // if this is for ssr then we give the full pathname
ssr
? pathname
: // otherwise it's to pull the currect assets out of our
// push-manifest and we give just /profile
"/profile"
: "/404";
};
We had some issues with seeing double rendering of async components and it seems to come from calling setState() when the component first renders. There is an open issue here: https://github.com/developit/preact-cli/issues/677
/profile
vs /profile/
and /profile/:user
flicker from the home route to the correct route because the service worker doesn't understand them. The /profile/
does not flicker. Would be nice to make these work without the flickering.preact.config.js
plugin to reduce data manipulation in the function. For example const mainBundleName = Object.keys(Assets).filter( key => key.includes("bundle") && key.includes("js") )[0];
should be done in the plugin. Also this could be made as a separate plugin and more easily installed.