mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +00:00
perf(nuxt): improve link prefetching (#8225)
This commit is contained in:
parent
2cf9009d33
commit
2a4ebfb18b
@ -1,7 +1,8 @@
|
|||||||
import { defineComponent, h, ref, resolveComponent, PropType, computed, DefineComponent, ComputedRef, onMounted, onBeforeUnmount } from 'vue'
|
import { defineComponent, h, ref, resolveComponent, PropType, computed, DefineComponent, ComputedRef, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import type { RouteLocationRaw, Router } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import { hasProtocol } from 'ufo'
|
import { hasProtocol } from 'ufo'
|
||||||
|
|
||||||
|
import { preloadRouteComponents } from '../composables/preload'
|
||||||
import { navigateTo, useRouter } from '../composables/router'
|
import { navigateTo, useRouter } from '../composables/router'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
|
|
||||||
@ -189,7 +190,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
|||||||
const el = process.server ? undefined : ref<HTMLElement | null>(null)
|
const el = process.server ? undefined : ref<HTMLElement | null>(null)
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
checkPropConflicts(props, 'prefetch', 'noPrefetch')
|
checkPropConflicts(props, 'prefetch', 'noPrefetch')
|
||||||
const shouldPrefetch = props.prefetch !== false && props.noPrefetch !== true && typeof to.value === 'string' && !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()
|
const observer = useObserver()
|
||||||
@ -269,7 +270,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return h('a', { href, rel, target }, slots.default?.())
|
return h('a', { ref: el, href, rel, target }, slots.default?.())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) as unknown as DefineComponent<NuxtLinkProps>
|
}) as unknown as DefineComponent<NuxtLinkProps>
|
||||||
@ -328,22 +329,3 @@ function isSlowConnection () {
|
|||||||
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true }
|
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preloadRouteComponents (to: string, router: Router & { _nuxtLinkPreloaded?: Set<string> } = useRouter()) {
|
|
||||||
if (process.server) { return }
|
|
||||||
|
|
||||||
if (!router._nuxtLinkPreloaded) { router._nuxtLinkPreloaded = new Set() }
|
|
||||||
if (router._nuxtLinkPreloaded.has(to)) { return }
|
|
||||||
router._nuxtLinkPreloaded.add(to)
|
|
||||||
|
|
||||||
const components = router.resolve(to).matched
|
|
||||||
.map(component => component.components?.default)
|
|
||||||
.filter(component => typeof component === 'function')
|
|
||||||
|
|
||||||
const promises: Promise<any>[] = []
|
|
||||||
for (const component of components) {
|
|
||||||
const promise = Promise.resolve((component as Function)()).catch(() => {})
|
|
||||||
promises.push(promise)
|
|
||||||
}
|
|
||||||
await Promise.all(promises)
|
|
||||||
}
|
|
||||||
|
@ -12,5 +12,5 @@ export type { CookieOptions, CookieRef } from './cookie'
|
|||||||
export { useRequestHeaders, useRequestEvent, setResponseStatus } from './ssr'
|
export { useRequestHeaders, useRequestEvent, setResponseStatus } from './ssr'
|
||||||
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, setPageLayout, navigateTo, useRoute, useActiveRoute, useRouter } from './router'
|
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, setPageLayout, navigateTo, useRoute, useActiveRoute, useRouter } from './router'
|
||||||
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
|
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
|
||||||
export { preloadComponents, prefetchComponents } from './preload'
|
export { preloadComponents, prefetchComponents, preloadRouteComponents } from './preload'
|
||||||
export { isPrerendered, loadPayload, preloadPayload } from './payload'
|
export { isPrerendered, loadPayload, preloadPayload } from './payload'
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
|
import type { Router } from 'vue-router'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
|
import { useRouter } from './router'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preload a component or components that have been globally registered.
|
* Preload a component or components that have been globally registered.
|
||||||
@ -31,3 +33,31 @@ function _loadAsyncComponent (component: Component) {
|
|||||||
return (component as any).__asyncLoader()
|
return (component as any).__asyncLoader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function preloadRouteComponents (to: string, router: Router & { _routePreloaded?: Set<string>; _preloadPromises?: Array<Promise<any>> } = useRouter()): Promise<void> {
|
||||||
|
if (process.server) { return }
|
||||||
|
|
||||||
|
if (!router._routePreloaded) { router._routePreloaded = new Set() }
|
||||||
|
if (router._routePreloaded.has(to)) { return }
|
||||||
|
router._routePreloaded.add(to)
|
||||||
|
|
||||||
|
const promises = router._preloadPromises ||= []
|
||||||
|
|
||||||
|
if (promises.length > 4) {
|
||||||
|
// Defer adding new preload requests until the existing ones have resolved
|
||||||
|
return Promise.all(promises).then(() => preloadRouteComponents(to, router))
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = router.resolve(to).matched
|
||||||
|
.map(component => component.components?.default)
|
||||||
|
.filter(component => typeof component === 'function')
|
||||||
|
|
||||||
|
for (const component of components) {
|
||||||
|
const promise = Promise.resolve((component as Function)())
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => promises.splice(promises.indexOf(promise)))
|
||||||
|
promises.push(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { parseURL } from 'ufo'
|
||||||
|
import { defineNuxtPlugin, useHead } from '#app'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const externalURLs = ref(new Set<string>())
|
||||||
|
useHead({
|
||||||
|
script: [
|
||||||
|
() => ({
|
||||||
|
type: 'speculationrules',
|
||||||
|
innerHTML: JSON.stringify({
|
||||||
|
prefetch: [
|
||||||
|
{
|
||||||
|
source: 'list',
|
||||||
|
urls: [...externalURLs.value],
|
||||||
|
requires: ['anonymous-client-ip-when-cross-origin']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
nuxtApp.hook('link:prefetch', (url) => {
|
||||||
|
const { protocol } = parseURL(url)
|
||||||
|
if (protocol && ['http:', 'https:'].includes(protocol)) {
|
||||||
|
externalURLs.value.add(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
@ -178,6 +178,11 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add experimental cross-origin prefetch support using Speculation Rules API
|
||||||
|
if (nuxt.options.experimental.crossOriginPrefetch) {
|
||||||
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/cross-origin-prefetch.client'))
|
||||||
|
}
|
||||||
|
|
||||||
// Track components used to render for webpack
|
// Track components used to render for webpack
|
||||||
if (nuxt.options.builder === '@nuxt/webpack-builder') {
|
if (nuxt.options.builder === '@nuxt/webpack-builder') {
|
||||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
|
||||||
|
@ -56,6 +56,7 @@ const appPreset = defineUnimportPreset({
|
|||||||
'updateAppConfig',
|
'updateAppConfig',
|
||||||
'defineAppConfig',
|
'defineAppConfig',
|
||||||
'preloadComponents',
|
'preloadComponents',
|
||||||
|
'preloadRouteComponents',
|
||||||
'prefetchComponents',
|
'prefetchComponents',
|
||||||
'loadPayload',
|
'loadPayload',
|
||||||
'preloadPayload',
|
'preloadPayload',
|
||||||
|
@ -79,5 +79,8 @@ export default defineUntypedSchema({
|
|||||||
* When this option is enabled (by default) payload of pages generated with `nuxt generate` are extracted
|
* When this option is enabled (by default) payload of pages generated with `nuxt generate` are extracted
|
||||||
*/
|
*/
|
||||||
payloadExtraction: true,
|
payloadExtraction: true,
|
||||||
|
|
||||||
|
/** Enable cross-origin prefetch using the Speculation Rules API. */
|
||||||
|
crossOriginPrefetch: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user