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/[email protected]. 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/[email protected] 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/. Ā 

GitHub - Cherry/workers-vue-ssr-example: Showcase Vue.js 3.2+ SSSR with Cloudflare Workers
Showcase Vue.js 3.2+ SSSR with Cloudflare Workers. Contribute to Cherry/workers-vue-ssr-example development by creating an account on GitHub.

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... šŸ˜…

You've successfully subscribed to James Ross
Great! Next, complete checkout for full access to James Ross
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.