fix(nuxt): defer render-blocking prefetches until after load (#9475)

This commit is contained in:
Daniel Roe 2022-12-02 16:13:35 +00:00 committed by GitHub
parent c26979a047
commit 52421a9354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 37 additions and 17 deletions

View File

@ -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() const shouldPrefetch = props.prefetch !== false && props.noPrefetch !== true && typeof to.value === 'string' && props.target !== '_blank' && !isSlowConnection()
if (shouldPrefetch) { if (shouldPrefetch) {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const observer = useObserver()
let idleId: number let idleId: number
let unobserve: Function | null = null let unobserve: Function | null = null
onMounted(() => { onMounted(() => {
idleId = requestIdleCallback(() => { const observer = useObserver()
if (el?.value?.tagName) { function registerCallback () {
unobserve = observer!.observe(el.value, async () => { idleId = requestIdleCallback(() => {
unobserve?.() if (el?.value?.tagName) {
unobserve = null unobserve = observer!.observe(el.value, async () => {
await Promise.all([ unobserve?.()
nuxtApp.hooks.callHook('link:prefetch', to.value as string).catch(() => {}), unobserve = null
!isExternal.value && preloadRouteComponents(to.value as string, router).catch(() => {}) await Promise.all([
]) nuxtApp.hooks.callHook('link:prefetch', to.value as string).catch(() => {}),
prefetched.value = true !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(() => { onBeforeUnmount(() => {
if (idleId) { cancelIdleCallback(idleId) } if (idleId) { cancelIdleCallback(idleId) }

View File

@ -142,6 +142,8 @@ export function createNuxtApp (options: CreateOptions) {
if (hydratingCount === 0) { if (hydratingCount === 0) {
nuxtApp.isHydrating = false nuxtApp.isHydrating = false
// @ts-expect-error private flag
globalThis.__hydrated = true
return nuxtApp.callHook('app:suspense:resolve') return nuxtApp.callHook('app:suspense:resolve')
} }
} }

View File

@ -1,27 +1,38 @@
import { join, resolve } from 'pathe' import { join, relative, resolve } from 'pathe'
import * as vite from 'vite' import * as vite from 'vite'
import vuePlugin from '@vitejs/plugin-vue' import vuePlugin from '@vitejs/plugin-vue'
import viteJsxPlugin from '@vitejs/plugin-vue-jsx' import viteJsxPlugin from '@vitejs/plugin-vue-jsx'
import type { ServerOptions } from 'vite' import type { ServerOptions } from 'vite'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
import { getPort } from 'get-port-please' import { getPort } from 'get-port-please'
import { joinURL, withoutLeadingSlash } from 'ufo' import { joinURL, withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
import defu from 'defu' import defu from 'defu'
import type { OutputOptions } from 'rollup' import type { OutputOptions } from 'rollup'
import { defineEventHandler } from 'h3' import { defineEventHandler } from 'h3'
import { genString } from 'knitwork'
import { cacheDirPlugin } from './plugins/cache-dir' import { cacheDirPlugin } from './plugins/cache-dir'
import type { ViteBuildContext, ViteOptions } from './vite' import type { ViteBuildContext, ViteOptions } from './vite'
import { devStyleSSRPlugin } from './plugins/dev-ssr-css' import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
import { viteNodePlugin } from './vite-node' import { viteNodePlugin } from './vite-node'
export async function buildClient (ctx: ViteBuildContext) { 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, { const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, {
entry: ctx.entry, entry: ctx.entry,
base: ctx.nuxt.options.dev base: ctx.nuxt.options.dev
? joinURL(ctx.nuxt.options.app.baseURL.replace(/^\.\//, '/') || '/', ctx.nuxt.options.app.buildAssetsDir) ? joinURL(ctx.nuxt.options.app.baseURL.replace(/^\.\//, '/') || '/', ctx.nuxt.options.app.buildAssetsDir)
: './', : './',
experimental: { 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') { if (hostType !== 'js' || type === 'asset') {
// In CSS we only use relative paths until we craft a clever runtime CSS hack // In CSS we only use relative paths until we craft a clever runtime CSS hack
return { relative: true } return { relative: true }