fix: Stop re-loading already-loaded CSS during server-side route resolution (#15014)
authorElliott Johnson <elliott.johnson@vercel.com>
Tue, 2 Dec 2025 20:57:46 +0000 (13:57 -0700)
committerGitHub <noreply@github.com>
Tue, 2 Dec 2025 20:57:46 +0000 (15:57 -0500)
* fix: Stop re-loading already-loaded CSS during server-side route resolution

* remove comments

* guarantee we only query the DOM once

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
.changeset/eager-rats-turn.md [new file with mode: 0644]
packages/kit/src/runtime/client/utils.js
packages/kit/test/apps/basics/test/cross-platform/client.test.js

diff --git a/.changeset/eager-rats-turn.md b/.changeset/eager-rats-turn.md
new file mode 100644 (file)
index 0000000..74828a9
--- /dev/null
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: Stop re-loading already-loaded CSS during server-side route resolution
index 5c77c80d22f03e723f32f90793f1ff9cc6373de3..4a62702b93890d1fc4f6f8073b158344dba8effa 100644 (file)
@@ -323,8 +323,8 @@ export function is_external_url(url, base, hash_routing) {
        return false;
 }
 
-/** @type {Record<string, boolean>} */
-const seen = {};
+/** @type {Set<string> | null} */
+let seen = null;
 
 /**
  * Used for server-side resolution, to replicate Vite's CSS loading behaviour in production.
@@ -341,13 +341,17 @@ export function load_css(deps) {
        );
        const csp_nonce = csp_nonce_meta?.nonce || csp_nonce_meta?.getAttribute('nonce');
 
+       seen ??= new Set(
+               Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map((link) => {
+                       return /** @type {HTMLLinkElement} */ (link).href;
+               })
+       );
+
        for (const dep of deps) {
-               if (dep in seen) continue;
-               seen[dep] = true;
+               const href = new URL(dep, document.baseURI).href;
 
-               if (document.querySelector(`link[href="${dep}"][rel="stylesheet"]`)) {
-                       continue;
-               }
+               if (seen.has(href)) continue;
+               seen.add(href);
 
                const link = document.createElement('link');
                link.rel = 'stylesheet';
index 0a04a1c05b63e19131eee3e8b5443aa662820c72..36b84c22eeb668fb2c30065e74556c45a5fbcb99 100644 (file)
@@ -927,9 +927,9 @@ test.describe('Routing', () => {
                // we start watching requests
                await page.goto('/routing/form-get', { waitUntil: 'load' });
 
-               expect(await page.textContent('h1')).toBe('...');
-               expect(await page.textContent('h2')).toBe('enter');
-               expect(await page.textContent('h3')).toBe('...');
+               await expect(page.locator('h1')).toHaveText('...');
+               await expect(page.locator('h2')).toHaveText('enter');
+               await expect(page.locator('h3')).toHaveText('...');
 
                /** @type {string[]} */
                const requests = [];
@@ -939,10 +939,10 @@ test.describe('Routing', () => {
                await page.locator('button').click();
 
                // Filter out server-side route resolution request
+               await expect(page.locator('h1')).toHaveText('updated');
+               await expect(page.locator('h2')).toHaveText('form');
+               await expect(page.locator('h3')).toHaveText('bar');
                expect(requests.filter((r) => !r.includes('__route.js'))).toEqual([]);
-               expect(await page.textContent('h1')).toBe('updated');
-               expect(await page.textContent('h2')).toBe('form');
-               expect(await page.textContent('h3')).toBe('bar');
        });
 
        test('responds to <form target="_blank"> submission with new tab', async ({ page }) => {