I got frustrated with how slow the Minima API was recently, and figured I'd just need to add a couple of database indexes before I could move on to the next thing.
That was not the case.
Nuxt Hub can be used to develop a Nuxt application running locally, against remote Cloudflare resources like D1. This is great for DX, but it also hid real-world performance issues from me until I started dogfooding Minima's public API.
In short, everything in your server bundle comes at a cost to the time it takes your Worker to start, and therefore your TTFB.
This is documented behavior but I hadn't correctly internalized it. I was under the impression that for frameworks like Nuxt, which automatically code split your server bundle, you were only paying the startup time for the main server chunk plus whatever else an endpoint might need.
Seeing this issue on GitHub was what it took for me to get it.
So how bad was it?
I created a test endpoint which didn't touch the database, just returned a typeid. It was taking between 700ms and 1,000ms to respond. Sad!
The Nuxt CLI has an analyze
command which generates a treemap of your production client and server bundles (just make sure you don't accidentally deploy the resulting build):
npx nuxt analyze
Armed with this, I started ripping pieces of the app out, deploying to workers.dev, and running ApacheBench on my test endpoint.
The main offenders were:
Shiki, which I was using for code highlighting in Tiptap
An endpoint which used a bunch of remark/rehype dependencies for parsing HTML into Markdown
The Vue REPL & Monaco Editor
Cloudflare's fork of Puppeteer
One massive (1.3MB) JSON file
Each time one of these things got ripped out it made a measurable difference, and with all of them gone, I was seeing API responses in under 100ms consistently.
Moral of the story: stay as close to 1MB gzipped as possible.
The best way to do full-stack Cloudflare right now appears to be to use individual workers as services. Unfortunately there's no workable solution for doing RPC or remote service bindings with Nuxt Hub yet, so I'm using a hacky "fetch in dev, RPC in prod" workaround for the time being.
One thing which was surprisingly challenging was getting code highlighting in Tiptap to work with a CDN build of any code highlighter.
Eventually I figured out that Tiptap's CodeBlockLowlight extension allowed you to pass in an instance of Lowlight, rather than just importing it, so it didn't need to be in my bundle.
I couldn't find anything on the web about using esm.sh imports with Vue/Nuxt, but eventually figured it out with trial and error. Then after refactoring from useEditor
to new Editor
in onMounted
I had something working, that looked like this:
onMounted(() => {
const { common, createLowlight } = await import('https://esm.sh/lowlight@3?bundle')
// note: no .then(module => module.default) needed here
editor = new Editor({
extensions: [
CodeBlockLowlight.configure({
lowlight,
}),
],
// ...
})
})