serverPrefetch sending API calls in series when used in nested components
Vue version
3.2.47
Link to minimal reproduction
Steps to reproduce
Context: I used Nuxt for simplicity since the utility useAsyncData
uses onServerPrefetch
under the hood to wait for the data before generating the HTML: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/asyncData.ts#L195
Open the codesandbox and click on the
dev:logs
tab.You'll see that the page setup function is evaluated first, then the fake API calls (with
setTimeout
) start in parallel, but the child setup function is only called once thepage query2
is resolved (the longest API calls).
What is expected?
I would expect to see all API calls awaited inside a serverPrefetch
to run in parallel no matter if it was registered from the parent or a nested child component.
I thought it could be fixed by creating an asyncHooks
array in renderToString
(@vue/server-renderer
) and pass it down to renderComponentVNode
and all other utilities in order to register every prefetch hook while evaluating the whole component tree without interruption. Once the first renderComponentVNode
returns, we await for all of them with Promise.all
:
https://github.com/vuejs/core/blob/main/packages/server-renderer/src/render.ts#L110
but it seems like the HTML is generated too early (when calling ssrRender
maybe?) since even with a timeout greater than all fake API calls together, it still doesn't generate any HTML:
const buffer = await renderComponentVNode(vnode)
await new Promise((resolve) => setTimeout(resolve, 6000))
const result = await unrollBuffer(buffer as SSRBuffer)
What is actually happening?
All API calls from the parent component are called in parallel, then only once they are all done, the setup function of the child component is called and its API call starts.
System Info
System:
OS: macOS 12.6.1
CPU: (10) arm64 Apple M1 Pro
Memory: 69.92 MB / 16.00 GB
Shell: 5.8.1 - /bin/zsh
Binaries:
Node: 18.14.0 - ~/.nvm/versions/node/v18.14.0/bin/node
Yarn: 1.22.19 - ~/.yarn/bin/yarn
npm: 9.3.1 - ~/.nvm/versions/node/v18.14.0/bin/npm
Browsers:
Brave Browser: 100.1.37.111
Chrome: 112.0.5615.137
Firefox: 112.0.1
Safari: 16.1
Any additional comments?
I discovered it by creating timelines based on the start and end time of each GraphQL queries we send during server-side rendering. The current results looks like this where you clearly see they are done in series:
Then I tried to stop awaiting within the serverPrefetch
hooks and even though it was not generating the expected HTML, all the queries were sent in parallel like I would expect:
This explained why SSR was performing so badly compared to the same tests we've done with SSR disabled in Nuxt config. We were thinking of moving all queries into the page for now, but we'll have to either accept the layout shift we'll have on some pages or we'll have to refactor a few pieces of our app.
I think the issue is urgent to fix considering the important performance hit it has on any SSR app setting ssrPrefetch
at different level (we were calling it from 2 different levels which almost doubled the response time). It is also mentioned in both the Vue SSR and Nuxt documentation that ssrPrefetch
or useAsyncData
can be used in nested components, but I would recommend avoiding it until this issue is fixed.
Anyway, I'm a big fan of Vue, thanks for making it happen ✌️. Let me know if I can help with a PR (a few pointers would be helpful though).
This is expected behavior - ssrPrefetch
is bound to the component lifecycle, and they only get called when the component is rendered by its parent. It has to work that way because ssrPrefetch
has access to the component's props - and props can only be resolved when the parent is rendered. Note this is not something to be "fixed": it's just how it was designed to work. Maybe we should make it more obvious that it can lead to request waterfalls and you should be careful when using it.
Collocating data fetching with components while having them run in parallel is an architectural problem - parallel data fetching is only feasible if your data fetching logic only depends on route parameters and nothing else. And for that it's better to do it at the route level. But overall this is more of a meta framework level concern that should be addressed by Nuxt, not Vue core.