Subscribe on changes!

serverPrefetch sending API calls in series when used in nested components

avatar
May 1st 2023

Vue version

3.2.47

Link to minimal reproduction

https://codesandbox.io/p/sandbox/modest-robinson-urdh3l?file=%2Fpackage.json&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A14%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A14%7D%5D

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 the page query2 is resolved (the longest API calls).

Screenshot 2023-05-01 at 12 05 20

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:

image

https://github.com/vuejs/core/blob/main/packages/server-renderer/src/render.ts#L110

image

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:

Screenshot 2023-05-01 at 10 17 11

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:

Screenshot 2023-05-01 at 10 20 18

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).

avatar
May 2nd 2023

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.