diff --git a/.changeset/pink-tigers-whisper.md b/.changeset/pink-tigers-whisper.md new file mode 100644 index 000000000000..6837fa592298 --- /dev/null +++ b/.changeset/pink-tigers-whisper.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: yield main thread before navigating diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index d39f3edbcf6e..d2c1de27f0a7 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -2027,7 +2027,7 @@ function _start_router() { } /** @param {MouseEvent} event */ - container.addEventListener('click', (event) => { + container.addEventListener('click', async (event) => { // Adapted from https://github.com/visionmedia/page.js // MIT license https://github.com/visionmedia/page.js#license if (event.button || event.which !== 1) return; @@ -2120,6 +2120,16 @@ function _start_router() { event.preventDefault(); + // allow the browser to repaint before navigating — + // this prevents INP scores being penalised + await new Promise((fulfil) => { + requestAnimationFrame(() => { + setTimeout(fulfil, 0); + }); + + setTimeout(fulfil, 100); // fallback for edge case where rAF doesn't fire because e.g. tab was backgrounded + }); + navigate({ type: 'link', url, diff --git a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte index ed56fb4855eb..e676c47a6533 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte @@ -6,6 +6,7 @@ a ok +next-paint symlinked elsewhere static.json diff --git a/packages/kit/test/apps/basics/src/routes/routing/next-paint/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/next-paint/+page.svelte new file mode 100644 index 000000000000..1aea392f7b8d --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/next-paint/+page.svelte @@ -0,0 +1 @@ +
next-paint
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 2a57fd5566cd..2d729e903adf 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1163,3 +1163,32 @@ test.describe('reroute', () => { expect(await page.textContent('h1')).toContain('Full Navigation'); }); }); + +test.describe('INP', () => { + test('does not block next paint', async ({ page }) => { + // Thanks to https://publishing-project.rivendellweb.net/measuring-performance-tasks-with-playwright/#interaction-to-next-paint-inp + async function measureInteractionToPaint(selector) { + return page.evaluate(async (selector) => { + return new Promise((resolve) => { + const startTime = performance.now(); + document.querySelector(selector).click(); + requestAnimationFrame(() => { + const endTime = performance.now(); + resolve(endTime - startTime); + }); + }); + }, selector); + } + + await page.goto('/routing'); + + const client = await page.context().newCDPSession(page); + await client.send('Emulation.setCPUThrottlingRate', { rate: 100 }); + + const time = await measureInteractionToPaint('a[href="/routing/next-paint"]'); + + // we may need to tweak this number, and the `rate` above, + // depending on if this proves flaky + expect(time).toBeLessThan(400); + }); +});