Critical Webpack Resource Injection for Vue 3 SSR Applications
Update: Apr 28, 2022
I've created a separate Webpack plugin to generate the manifest of critical assets without any of the limitations from my original solution below. It consists of a client plugin and server plugin similar to how SSR worked in Vue 2.
Vue 3 has been officially released for almost a year but presently, at the time of writing this post, its Server Side Rendering (SSR) support is still very lackluster — unless you use an opinionated toolchain like Nuxt 3 (still not released), Quasar 2, or Vite — you're somewhat screwed as a Webpack user.
Even without this list of critical CSS, as long as the SSR markup includes the main entry point
Consider the following component hierarchy for a hypothetical Vue 3 SSR website:
If we simply render
I didn't follow Vue 3's development too closely so I can only speculate based on my experience of reading the source code. Optimistically, I believe it's due to Vue 3 wanting to become toolchain-agnostic. Back in Vue 2, everything was assumed to be bundled with Webpack. In Vue 3, we have Vite and Webpack for bundling Vue applications (I guess Parcel and Gulp also exists but who uses those anymore?). As a result, it would not make sense for Vue 3's SSR renderer (
renderToString) to accept a Webpack-specific
manifest.json to generate the list of critical resources.
The resource <URL> was preloaded using link preload but not used within a few seconds from the window's load event.
Please make sure it has an appropriate `as` value and it is preloaded intentionally.
One thing I noticed while playing around with Vue 3 is that we can already determine what components the current route will load without any additional toolchain support.
Unfortunately, this isn't particularly useful because this function only returns a list of components and not their actual output files.
Our goal is to thus map this list of components into their actual output files. One important observation here is that each component object has a
__file property. By default,
vue-loader only injects this property in development builds. Luckily, we can configure the loader to insert this property in production builds as well by setting
exposeFilename: true in our Webpack configuration.
MainLayout.vue could correspond to
MainLayout.js. If we can force Webpack to output files (also known as "chunks") in this manner, then it would be a trivial task of manipulating each component's
__file property to get the output file names.
For large components, this is already Webpack's default behavior: each component's id (inside Webpack) is just their full path with slashes and dots replaced by underscores e.g.
src_web_layouts_MainLayout_MainLayout_vue. All we have to do for this case is to pass a function into Webpack's
output.filename config to convert this name into
One optimization Webpack performs is merging multiple small files into a single large file. The problem is that this new file has an incomprehensible name that does not tell us what Vue components are located inside of it. We can resolve this by configuring Webpack's chunking algorithm to split every file regardless of the output file's size so that it never merges any components.
Finally putting everything together in our HTTP handler:
- Your component file names must be globally unique. For example, you cannot have multiple components named
Header.vuein different directories since
createOutputNameFnonly returns the file name.
Obviously this isn't a problem if your source code is already open source. For closed-source projects, this is something to consider but it shouldn't be an issue as long as your directory or file names are not confidential or politically-sensitive.
getMatchedComponentscannot determine which asynchronous components are loaded. As a result, even though they are still be rendered on the server by
defineAsyncComponentin SSR applications).
Finally, you can only have one entry point in your Webpack config.
If you try to add another entry point (e.g. for a service worker), Webpack would combine any shared vendor code between all your entry points into a new vendor file. As a result,
createOutputNameFnwould try to output multiple initial vendor files with the same name thus throwing an exception.
This shouldn't be a big issue since most projects should only need one entry point per configuration. If your SSR application does have multiple entry points, you would have to instead duplicate your configuration and output each entry point to a different directory.