Server Side Rendering Vue.js 3.2+ on Cloudflare Workers with pipeToWebWritable
Up to this point, server side rendering (SSR) Vue 3 on Cloudflare Workers has not been easy (or very efficient). However, with the addition of various streaming capabilities such as renderToWebStream
and pipeToWebWritable
in Vue.js 3.2, this is changing.
renderToWebStream
is a new API available in @vue/server-renderer
that lets us render a Vue SSR app directly to a Web ReadableStream, which is something that Cloudflare Workers (almost) understand natively, and can efficiently serve to the client!
UPDATE: As of version 3.2.0-beta.8
, a new API pipeToWebWritable
is also available, which makes this entire process much easier and interfaces with TransportStream
. As of version 3.2.2
, this API is now stable and works perfectly in Cloudflare Workers!
Proof of Concept
My initial proof of concept was a very simple Vue app, using nothing but createSSRApp
, and an inline render function.
Beta Testing
This testing was done using version 3.2.0-beta.7
of both vue
and @vue/server-renderer
. It effectively looked like this:
import {h, createSSRApp} from 'vue';
import {renderToWebStream} from '@vue/server-renderer';
async function handleRequest(request, env){
const app = createSSRApp({
data: () => {
return {
msg: 'Hello World from Vue.js SSR!',
};
},
render(){
return h('div', [
this.msg,
]);
},
});
return new Response(renderToWebStream(app));
}
export default {
async fetch(request, env){
try{
return await handleRequest(request, env);
}catch(err){
return new Response(err.message);
}
},
};
In theory, this should have "just worked", but unfortunately Cloudflare Workers does not implement the ReadableStream
constructor which renderToWebStream
requires. No matter though, we can work around that by ponyfilling it, and using a TransformStream
, which is supported by Cloudflare Workers! 😄
After a lot of debugging, and guidance from the Vue team, I had something that worked, and still maintained the streaming functionality! The following example can only be used with @vue/server-renderer@3.2.0-beta.7
. Read on for how to do it in future versions.
import {h, createSSRApp} from 'vue';
import {renderToWebStream} from '@vue/server-renderer';
import {ReadableStream as polyReadableStream} from 'web-streams-polyfill/ponyfill/es6';
async function pipeReaderToWriter(reader, writer){
const encoder = new TextEncoder();
for(;;){
const {value, done} = await reader.read();
await writer.write(encoder.encode(value));
if(done){
break;
}
}
writer.close();
}
async function handleRequest(request, env){
const app = createSSRApp({
data: () => {
return {
msg: 'Hello World from Vue.js SSR!',
};
},
render(){
return h('div', [
this.msg,
]);
},
});
const {readable, writable} = new TransformStream();
const render = renderToWebStream(app, {}, polyReadableStream);
pipeReaderToWriter(render.getReader(), writable.getWriter());
return new Response(readable);
}
export default {
async fetch(request, env){
try{
return handleRequest(request, env);
}catch(err){
return new Response(err.message);
}
},
};
It was a little more verbose than I'd like, but it worked! 🎉
Vue.js 3.2.0-beta.8
and higher
As of @vue/server-renderer@3.2.0-beta.8
and higher, there is now a new API pipeToWebWritable
, which allows us to use a TransformStream
directly, without having to ponyfill anything or manually iterate over the stream. It now looks like this:
import {createSSRApp} from 'vue';
import {pipeToWebWritable} from '@vue/server-renderer';
import appVue from './app.vue';
async function handleRequest(request, env){
const app = createSSRApp(appVue);
const {readable, writable} = new TransformStream();
pipeToWebWritable(app, {}, writable);
return new Response(readable);
}
export default {
async fetch(request, env){
try{
return handleRequest(request, env);
}catch(err){
return new Response(err.message);
}
},
};
There were a few issues with this in the initial betas, but as of 3.2.2
, this now works perfectly and server side rendering Vue.js apps in Cloudflare Workers has never been easier! 😁
Real-world Test
Now that I had a proof of concept, I wanted to expand this and showcase a more real-world application using a few .vue
Single File Components (SFCs). Fundamentally the worker code is the same as above, but I've extended the build webpack configuration to support .vue
SFCs, and made this as seamless as possible by using Wrangler Custom Builds.
My webpack config ended up looking something like this (condensed for brevity):
export default function(){
const config = {
target: "webworker",
entry: "./src/index.mjs",
output: {
path: path.resolve(__dirname, 'dist'),
filename: "index.mjs",
chunkFormat: 'array-push',
library: {
type: 'module',
},
},
module: {
rules: [
{
test: /\.mjs$/,
type: 'javascript/esm',
},
{
test: /\.vue$/,
loader: 'vue-loader',
},
],
},
plugins: [
new VueLoaderPlugin(),
],
experiments: {
outputModule: true,
},
};
return config;
}
You can find the full project and code in the GitHub repository below, including the full webpack config, worker code, and example components. A (very primitive) live version can also be found at https://vue-ssr-poc.jross.workers.dev/.
Expansion
Now that I have a working proof of concept with SSR in Cloudflare Workers, it's time to start rehydrating the application after it's served so we can do more fun client-side side things, but that's a topic for another day... 😅