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