Vite workerd ssr request handler experimentation

This repository shows an experimentation which consists in making a preact application be server side rendered using the Cloudflare workerd runtime when run locally with the Vite dev server.

To run the application

Install the dependencies:

pnpm i

and simply run the preact app with:

pnpm preact:dev

Details

In this experimentation we've created a new vite plugin preact-workerd-ssr-plugin and added it to the app's Vite config plugins field.

Note regarding the plugin

Instead of creating a new plugin we could have modified the existing preact one instead, we didn't simply for simplicity and because preact's implementation allows it.

We've experimented with other frameworks as well and in other cases it might be necessary to add this to the Vite plugin itself, alongside other minor changes, in order to not conflict with its behavior.


What the new plugin does is to intercept (using a middleware) incoming requests and instead of having them handled in the standard Node.js runtime passes them to a handler we defined that handles them (/performs server side rendering) in the workerd runtime instead, afterwords the response from the handler is simply used to return an HTML response to the user.

The above mentioned handler is created using the createWorkerdHandler function from the vite-workerd-request-handler package (note: this doesn't really need to be its own standalone package, we did it so that it would be easier to reuse when experimenting with other frameworks).

createWorkerdHandler

The createWorkerdHandler takes three values as inputs:

  • entrypoint: the entrypoint file for the application
  • server: the viteDevServer, which the handler needs to integrate with
  • requestHandler: framework specific logic on how requests should be handled

What the function then does is:

How the code runs in workerd

When we instantiate the miniflare instance we pass to it a specific script that allows it to interact and lazily load modules from the Vite dev server.

Note regarding replaced values

In the script WORKERD_APP_ENTRYPOINT, __REQUEST_HANDLER__ and VITE_SERVER_ADDRESS are simply placeholders that we replace with the actual proper values when instantiating miniflare.


Such dynamic import behavior is implemented via the UNSAFE_EVAL binding which allows us to evaluate code in workerd, what we basically do is providing our own implementation of the Vite module resolution functions (__vite_ssr_import__, __vite_ssr_dynamic_import__, etc...) which interact with the Vite dev server and when necessary fetch module code (thanks to the "workerd loader" we set up earlier) and evaluate it (we took inspiration for the above from vite-node as the concept is pretty similar).

Potential issues with this approach

  • this approach relies on the framework providing/allowing a single clean handler that takes a request and generate/returns a response, other frameworks (such as qwik) can do more complex things and effectively needing to run server side code throughout different parts of the request handling process making this approach not applicable (we also experimented with a more complex variation of this approach which does allow for such more complex request processing, at the cost of a more complex architecture)
  • Vite module resolution allows for modules to use cjs as well as the Vite specific module resolution functions, in workerd we do provide out own implementation for the Vite functions but we cannot handle cjs modules there, this is a limitation that needs to be kept in mind