From 5e765874901d49ff258b3aa45a2305715a050419 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 20 Jun 2024 23:18:57 +0100 Subject: [PATCH] fix(nuxt): delay navigation until user input is acknowledged (#27743) --- .../app/plugins/navigation-repaint.client.ts | 19 +++++++++++++++++++ packages/nuxt/src/core/nuxt.ts | 6 ++++++ packages/nuxt/test/app.test.ts | 4 ++++ packages/schema/src/config/experimental.ts | 8 ++++++++ 4 files changed, 37 insertions(+) create mode 100644 packages/nuxt/src/app/plugins/navigation-repaint.client.ts 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 8952a1f7df..d5fe6f8740 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -532,6 +532,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 2e66d85bdf..c7424e95ad 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 c1d903ba5d..a9b9b26dcd 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -417,5 +417,13 @@ export default defineUntypedSchema({ * @type {boolean} */ clientNodeCompat: false, + + /** + * 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, }, })