From 96f07a4c1c55c0253de8db9a25f65bd27c5d9b33 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 8 Jun 2022 11:37:53 +1000 Subject: [PATCH 1/3] fix(dev): fix FOUC when swapping out link tag Assuming we have a CSS file at the following path... ``` /resources/css/app.css ``` When adding a link tag to the DOM manually (i.e. via Server Side Rendering)... ```html ``` Vite will now watch this file and hot reload changes in the browser for this asset. When we update `app.css` Vite will update the link tag's `href` attribute like so... ```diff - + ``` This is great, but for a split moment there is a flash of unstyled content as the asset has not loaded yet. The browser unloads the styles from the original stylesheet and waits until the new stylesheet has loaded and been parsed before it will show the new stylings. This PR addresses this problem. Now the process is as follows.. First the client adds a second (cloned) link tag to the DOM, leaving the original in place and still loaded by the browser... ```diff + ``` Once the new stylesheet has loaded, the browser will remove the original stylesheet from the DOM, leaving the new _loaded_ stylesheet in the DOM. ```diff - ``` This process then continues for future updates to the CSS file. I have no idea where to start adding a unit test for this. I'm a dumb back-end developer. Send help! Edit: i've added a test. Not sure if it is any good though! --- packages/vite/src/client/client.ts | 11 +++++++- playground/hmr/__tests__/hmr.spec.ts | 14 ++++++++++ playground/hmr/hmr.ts | 42 ++++++++++++++++++++++++---- playground/hmr/index.html | 10 ++++++- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 5aec1eae9c5bc0..dc7728c69711c0 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -101,7 +101,16 @@ async function handleMessage(payload: HMRPayload) { const newPath = `${base}${searchUrl.slice(1)}${ searchUrl.includes('?') ? '&' : '?' }t=${timestamp}` - el.href = new URL(newPath, el.href).href + + // rather than swapping the href on the existing tag, we will + // create a new link tag. Once the new stylesheet has loaded we + // will remove the existing link tag. This removes a Flash Of + // Unstyled Content that can occur when swapping out the tag href + // directly, as the new stylesheet has not yet been loaded. + const newLinkTag = el.cloneNode() as HTMLLinkElement + newLinkTag.href = new URL(newPath, el.href).href + newLinkTag.addEventListener('load', () => el.remove()) + el.after(newLinkTag) } console.log(`[vite] css hot updated: ${searchUrl}`) } diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index d06fee31f110ea..2ccdf96d84b970 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -169,6 +169,20 @@ if (!isBuild) { expect(textpost).not.toMatch('direct') }) + test('it swaps out link tags', async () => { + await page.goto(viteTestUrl) + + editFile('global.css', (code) => code.replace('white', 'tomato')) + + let el = await page.$('.link-tag-added') + await untilUpdated(() => el.textContent(), 'yes') + + el = await page.$('.link-tag-removed') + await untilUpdated(() => el.textContent(), 'yes') + + expect((await page.$$('link')).length).toBe(1) + }) + test('not loaded dynamic import', async () => { await page.goto(viteTestUrl + '/counter/index.html') diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts index 113b87bc5865d4..a986db769f51fe 100644 --- a/playground/hmr/hmr.ts +++ b/playground/hmr/hmr.ts @@ -41,12 +41,42 @@ if (import.meta.hot) { update.type === 'css-update' && update.path.match('global.css') ) if (cssUpdate) { - const el = document.querySelector('#global-css') as HTMLLinkElement - text('.css-prev', el.href) - // We don't have a vite:afterUpdate event, but updates are currently sync - setTimeout(() => { - text('.css-post', el.href) - }, 0) + text( + '.css-prev', + (document.querySelector('.global-css') as HTMLLinkElement).href + ) + + // We don't have a vite:afterUpdate event, but updates are currently + // sync. We need to wait until the tag has been swapped out, which + // includes the time taken to download and parse the new stylesheet. + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if ( + node.nodeType === Node.ELEMENT_NODE && + (node as Element).tagName === 'LINK' + ) { + text('.link-tag-added', 'yes') + } + }) + mutation.removedNodes.forEach((node) => { + if ( + node.nodeType === Node.ELEMENT_NODE && + (node as Element).tagName === 'LINK' + ) { + text('.link-tag-removed', 'yes') + text( + '.css-post', + (document.querySelector('.global-css') as HTMLLinkElement).href + ) + } + }) + }) + }) + + observer.observe(document.querySelector('#style-tags-wrapper'), { + childList: true + }) } }) diff --git a/playground/hmr/index.html b/playground/hmr/index.html index 65a2ed381b027a..7857ef818a911c 100644 --- a/playground/hmr/index.html +++ b/playground/hmr/index.html @@ -1,4 +1,10 @@ - +
+ +