diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index ee9225da2d..7b1e5821de 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -193,23 +193,30 @@ export function defineNuxtLink (options: NuxtLinkOptions) { const shouldPrefetch = props.prefetch !== false && props.noPrefetch !== true && typeof to.value === 'string' && props.target !== '_blank' && !isSlowConnection() if (shouldPrefetch) { const nuxtApp = useNuxtApp() - const observer = useObserver() let idleId: number let unobserve: Function | null = null onMounted(() => { - idleId = requestIdleCallback(() => { - if (el?.value?.tagName) { - unobserve = observer!.observe(el.value, async () => { - unobserve?.() - unobserve = null - await Promise.all([ - nuxtApp.hooks.callHook('link:prefetch', to.value as string).catch(() => {}), - !isExternal.value && preloadRouteComponents(to.value as string, router).catch(() => {}) - ]) - prefetched.value = true - }) - } - }) + const observer = useObserver() + function registerCallback () { + idleId = requestIdleCallback(() => { + if (el?.value?.tagName) { + unobserve = observer!.observe(el.value, async () => { + unobserve?.() + unobserve = null + await Promise.all([ + nuxtApp.hooks.callHook('link:prefetch', to.value as string).catch(() => {}), + !isExternal.value && preloadRouteComponents(to.value as string, router).catch(() => {}) + ]) + prefetched.value = true + }) + } + }) + } + if (nuxtApp.isHydrating) { + nuxtApp.hooks.hookOnce('app:suspense:resolve', registerCallback) + } else { + registerCallback() + } }) onBeforeUnmount(() => { if (idleId) { cancelIdleCallback(idleId) } diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index a6bd2aa22d..59e2b67883 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -142,6 +142,8 @@ export function createNuxtApp (options: CreateOptions) { if (hydratingCount === 0) { nuxtApp.isHydrating = false + // @ts-expect-error private flag + globalThis.__hydrated = true return nuxtApp.callHook('app:suspense:resolve') } } diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index 1615db5ceb..0f498dccfc 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -1,27 +1,38 @@ -import { join, resolve } from 'pathe' +import { join, relative, resolve } from 'pathe' import * as vite from 'vite' import vuePlugin from '@vitejs/plugin-vue' import viteJsxPlugin from '@vitejs/plugin-vue-jsx' import type { ServerOptions } from 'vite' import { logger } from '@nuxt/kit' import { getPort } from 'get-port-please' -import { joinURL, withoutLeadingSlash } from 'ufo' +import { joinURL, withoutLeadingSlash, withoutTrailingSlash } from 'ufo' import defu from 'defu' import type { OutputOptions } from 'rollup' import { defineEventHandler } from 'h3' +import { genString } from 'knitwork' import { cacheDirPlugin } from './plugins/cache-dir' import type { ViteBuildContext, ViteOptions } from './vite' import { devStyleSSRPlugin } from './plugins/dev-ssr-css' import { viteNodePlugin } from './vite-node' export async function buildClient (ctx: ViteBuildContext) { + const buildAssetsDir = withoutLeadingSlash( + withoutTrailingSlash(ctx.nuxt.options.app.buildAssetsDir) + ) + const relativeToBuildAssetsDir = (filename: string) => './' + relative(buildAssetsDir, filename) const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, { entry: ctx.entry, base: ctx.nuxt.options.dev ? joinURL(ctx.nuxt.options.app.baseURL.replace(/^\.\//, '/') || '/', ctx.nuxt.options.app.buildAssetsDir) : './', experimental: { - renderBuiltUrl: (filename, { type, hostType }) => { + renderBuiltUrl: (filename, { type, hostType, hostId }) => { + // When rendering inline styles, we can skip loading CSS chunk that matches the current page + if (ctx.nuxt.options.experimental.inlineSSRStyles && hostType === 'js' && filename.endsWith('.css')) { + return { + runtime: `!globalThis.__hydrated ? ${genString(relativeToBuildAssetsDir(hostId))} : ${genString(relativeToBuildAssetsDir(filename))}` + } + } if (hostType !== 'js' || type === 'asset') { // In CSS we only use relative paths until we craft a clever runtime CSS hack return { relative: true }