diff --git a/packages/nuxt/src/app/plugins/navigation-repaint.client.ts b/packages/nuxt/src/app/plugins/navigation-repaint.client.ts new file mode 100644 index 0000000000..30f3a69cf1 --- /dev/null +++ b/packages/nuxt/src/app/plugins/navigation-repaint.client.ts @@ -0,0 +1,19 @@ +import { defineNuxtPlugin } from '../nuxt' +import { useRouter } from '../composables' + +export default defineNuxtPlugin(() => { + useRouter().beforeResolve(async () => { + /** + * This gives an opportunity for the browser to repaint, acknowledging user interaction. + * It can reduce INP when navigating on prerendered routes. + * + * @see https://github.com/nuxt/nuxt/issues/26271#issuecomment-2178582037 + * @see https://vercel.com/blog/demystifying-inp-new-tools-and-actionable-insights + */ + await new Promise((resolve) => { + // Ensure we always resolve, even if the animation frame never fires + setTimeout(resolve, 100) + requestAnimationFrame(() => { setTimeout(resolve, 0) }) + }) + }) +}) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 2cab93f2c7..27a2ea6f89 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -533,6 +533,12 @@ async function initNuxt (nuxt: Nuxt) { } } + if (nuxt.options.experimental.navigationRepaint) { + addPlugin({ + src: resolve(nuxt.options.appDir, 'plugins/navigation-repaint.client'), + }) + } + nuxt.hooks.hook('builder:watch', (event, relativePath) => { const path = resolve(nuxt.options.srcDir, relativePath) // Local module patterns diff --git a/packages/nuxt/test/app.test.ts b/packages/nuxt/test/app.test.ts index 3673d711f2..7370e493bc 100644 --- a/packages/nuxt/test/app.test.ts +++ b/packages/nuxt/test/app.test.ts @@ -43,6 +43,10 @@ describe('resolveApp', () => { "mode": "client", "src": "/packages/nuxt/src/app/plugins/payload.client.ts", }, + { + "mode": "client", + "src": "/packages/nuxt/src/app/plugins/navigation-repaint.client.ts", + }, { "mode": "client", "src": "/packages/nuxt/src/app/plugins/check-outdated-build.client.ts", diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index b80f0beca1..47e292dff2 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -513,5 +513,13 @@ export default defineUntypedSchema({ return val ?? ((await get('future') as Record).compatibilityVersion !== 4) }, }, + + /** + * Wait for a single animation frame before navigation, which gives an opportunity + * for the browser to repaint, acknowledging user interaction. + * + * It can reduce INP when navigating on prerendered routes. + */ + navigationRepaint: true, }, })