diff --git a/docs/2.guide/3.going-further/1.experimental-features.md b/docs/2.guide/3.going-further/1.experimental-features.md
index c244dab8b..23cc9c52b 100644
--- a/docs/2.guide/3.going-further/1.experimental-features.md
+++ b/docs/2.guide/3.going-further/1.experimental-features.md
@@ -321,7 +321,41 @@ You can also set this to `chokidar` to watch all files in your source directory.
```ts [nuxt.config.ts]
export defineNuxtConfig({
experimental: {
- watcher: 'chokidar-granular' // 'chokidar' or 'parcel' are also options
+ watcher: 'chokidar-granular' // 'chokidar' or 'parcel' are also options
}
})
```
+
+## sharedPrerenderData
+
+Enabling this feature automatically shares payload *data* between pages that are prerendered. This can result
+in a significant performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and
+fetch the same data in different pages.
+
+```ts [nuxt.config.ts]
+export defineNuxtConfig({
+ experimental: {
+ sharedPrerenderData: true
+ }
+})
+```
+
+Note that by default Nuxt will render pages concurrently, meaning this does not guarantee that data will
+not be fetched more than once.
+
+It is particularly important when enabling this feature to make sure that any unique key of your data
+is always resolvable to the same data. For example, if you are using `useAsyncData` to fetch
+data related to a particular page, you should provide a key that uniquely matches that data. (`useFetch`
+should do this automatically for you.)
+
+```ts
+// This would be unsafe in a dynamic page (e.g. `[slug].vue`) because the route slug makes a difference
+// to the data fetched, but Nuxt can't know that because it's not reflected in the key.
+const route = useRoute()
+const { data } = await useAsyncData(async () => {
+ return await $fetch(`/api/my-page/${route.params.slug}`)
+})
+// Instead, you should use a key that uniquely identifies the data fetched.
+const { data } = await useAsyncData(route.params.slug, async () => {
+ return await $fetch(`/api/my-page/${route.params.slug}`)
+})
diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts
index 66dca1792..bbdcaabb9 100644
--- a/packages/nuxt/src/app/composables/asyncData.ts
+++ b/packages/nuxt/src/app/composables/asyncData.ts
@@ -3,7 +3,7 @@ import type { Ref, WatchSource } from 'vue'
import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt'
import { toArray } from '../utils'
-import type { NuxtError} from './error';
+import type { NuxtError } from './error'
import { createError } from './error'
import { onNuxtReady } from './ready'
@@ -17,18 +17,18 @@ export type _Transform = (input: Input) => Output
export type PickFrom> = T extends Array
? T
: T extends Record
- ? keyof T extends K[number]
- ? T // Exact same keys as the target, skip Pick
- : K[number] extends never
- ? T
- : Pick
- : T
+ ? keyof T extends K[number]
+ ? T // Exact same keys as the target, skip Pick
+ : K[number] extends never
+ ? T
+ : Pick
+ : T
export type KeysOf = Array<
T extends T // Include all keys of union types, not just common keys
? keyof T extends string
- ? keyof T
- : never
+ ? keyof T
+ : never
: never
>
@@ -135,19 +135,29 @@ export function useAsyncData<
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
// eslint-disable-next-line prefer-const
- let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions]
+ let [key, _handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions]
// Validate arguments
if (typeof key !== 'string') {
throw new TypeError('[nuxt] [asyncData] key must be a string.')
}
- if (typeof handler !== 'function') {
+ if (typeof _handler !== 'function') {
throw new TypeError('[nuxt] [asyncData] handler must be a function.')
}
// Setup nuxt instance payload
const nuxt = useNuxtApp()
+ // When prerendering, share payload data automatically between requests
+ const handler = import.meta.client || !import.meta.prerender || !nuxt.ssrContext?._sharedPrerenderCache ? _handler : async () => {
+ const value = await nuxt.ssrContext!._sharedPrerenderCache!.get(key)
+ if (value) { return value as ResT }
+
+ const promise = nuxt.runWithContext(_handler)
+ nuxt.ssrContext!._sharedPrerenderCache!.set(key, promise)
+ return promise
+ }
+
// Used to get default values
const getDefault = () => null
const getDefaultCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts
index 28468fccb..553faaf95 100644
--- a/packages/nuxt/src/app/nuxt.ts
+++ b/packages/nuxt/src/app/nuxt.ts
@@ -69,6 +69,11 @@ export interface NuxtSSRContext extends SSRContext {
_renderResponse?: Partial
/** @internal */
_payloadReducers: Record any>
+ /** @internal */
+ _sharedPrerenderCache?: {
+ get (key: string): Promise
+ set (key: string, value: Promise): Promise
+ }
}
export interface NuxtPayload {
diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts
index 6400b19e4..c0ca416fa 100644
--- a/packages/nuxt/src/core/nitro.ts
+++ b/packages/nuxt/src/core/nitro.ts
@@ -218,6 +218,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
'process.env.NUXT_JSON_PAYLOADS': !!nuxt.options.experimental.renderJsonPayloads,
'process.env.NUXT_COMPONENT_ISLANDS': !!nuxt.options.experimental.componentIslands,
'process.env.NUXT_ASYNC_CONTEXT': !!nuxt.options.experimental.asyncContext,
+ 'process.env.NUXT_SHARED_DATA': !!nuxt.options.experimental.sharedPrerenderData,
'process.dev': nuxt.options.dev,
__VUE_PROD_DEVTOOLS__: false
},
diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index c2695971c..b1e4c20fd 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -183,6 +183,21 @@ const getSPARenderer = lazyCachedFunction(async () => {
const payloadCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:payload') : null
const islandCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island') : null
const islandPropCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island-props') : null
+const sharedPrerenderPromises = import.meta.prerender && process.env.NUXT_SHARED_DATA ? new Map>() : null
+const sharedPrerenderCache = import.meta.prerender && process.env.NUXT_SHARED_DATA ? {
+ get (key: string): Promise {
+ if (sharedPrerenderPromises!.has(key)) {
+ return sharedPrerenderPromises!.get(key)!
+ }
+ return useStorage('internal:nuxt:prerender:shared').getItem(key) as Promise
+ },
+ async set (key: string, value: Promise) {
+ sharedPrerenderPromises!.set(key, value)
+ return useStorage('internal:nuxt:prerender:shared').setItem(key, await value as any)
+ // free up memory after the promise is resolved
+ .finally(() => sharedPrerenderPromises!.delete(key))
+ },
+} : null
async function getIslandContext (event: H3Event): Promise {
// TODO: Strict validation for url
@@ -287,6 +302,10 @@ export default defineRenderHandler(async (event): Promise {
+ * return await $fetch(`/api/my-page/${route.params.slug}`)
+ * })
+ * // Instead, you should use a key that uniquely identifies the data fetched.
+ * const { data } = await useAsyncData(route.params.slug, async () => {
+ * return await $fetch(`/api/my-page/${route.params.slug}`)
+ * })
+ */
+ sharedPrerenderData: false,
+
/**
* This allows specifying the default options for core Nuxt components and composables.
*