feat(nuxt): add experimental sharedPrerenderData option (#24894)

This commit is contained in:
Daniel Roe 2024-01-18 10:01:39 +00:00 committed by GitHub
parent e44e8b35dd
commit 210a559350
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 108 additions and 12 deletions

View File

@ -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}`)
})

View File

@ -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 = any, Output = any> = (input: Input) => Output
export type PickFrom<T, K extends Array<string>> = T extends Array<any>
? T
: T extends Record<string, any>
? keyof T extends K[number]
? T // Exact same keys as the target, skip Pick
: K[number] extends never
? T
: Pick<T, K[number]>
: T
? keyof T extends K[number]
? T // Exact same keys as the target, skip Pick
: K[number] extends never
? T
: Pick<T, K[number]>
: T
export type KeysOf<T> = 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<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
let [key, _handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
// 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]

View File

@ -69,6 +69,11 @@ export interface NuxtSSRContext extends SSRContext {
_renderResponse?: Partial<RenderResponse>
/** @internal */
_payloadReducers: Record<string, (data: any) => any>
/** @internal */
_sharedPrerenderCache?: {
get<T = unknown> (key: string): Promise<T>
set<T> (key: string, value: Promise<T>): Promise<void>
}
}
export interface NuxtPayload {

View File

@ -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
},

View File

@ -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<string, Promise<any>>() : null
const sharedPrerenderCache = import.meta.prerender && process.env.NUXT_SHARED_DATA ? {
get <T = unknown>(key: string): Promise<T> {
if (sharedPrerenderPromises!.has(key)) {
return sharedPrerenderPromises!.get(key)!
}
return useStorage('internal:nuxt:prerender:shared').getItem(key) as Promise<T>
},
async set <T>(key: string, value: Promise<T>) {
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<NuxtIslandContext> {
// TODO: Strict validation for url
@ -287,6 +302,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
islandContext
}
if (import.meta.prerender && process.env.NUXT_SHARED_DATA) {
ssrContext._sharedPrerenderCache = sharedPrerenderCache!
}
// Whether we are prerendering route
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(useRuntimeConfig().app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') : undefined

View File

@ -259,6 +259,33 @@ export default defineUntypedSchema({
*/
inlineRouteRules: false,
/**
* Automatically share 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.
*
* 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.)
* @example
* ```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}`)
* })
*/
sharedPrerenderData: false,
/**
* This allows specifying the default options for core Nuxt components and composables.
*