Skip to content

Commit

Permalink
fix(dev): avoid FOUC when swapping out link tag (fix #7973) (#8495)
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald committed Jun 9, 2022
1 parent 8d08220 commit 0e5c009
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 8 deletions.
13 changes: 12 additions & 1 deletion packages/vite/src/client/client.ts
Expand Up @@ -101,7 +101,18 @@ 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
const removeOldEl = () => el.remove()
newLinkTag.addEventListener('load', removeOldEl)
newLinkTag.addEventListener('error', removeOldEl)
el.after(newLinkTag)
}
console.log(`[vite] css hot updated: ${searchUrl}`)
}
Expand Down
14 changes: 14 additions & 0 deletions playground/hmr/__tests__/hmr.spec.ts
Expand Up @@ -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')

Expand Down
42 changes: 36 additions & 6 deletions playground/hmr/hmr.ts
Expand Up @@ -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.
// 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
})
}
})

Expand Down
10 changes: 9 additions & 1 deletion playground/hmr/index.html
@@ -1,4 +1,10 @@
<link id="global-css" rel="stylesheet" href="./global.css?param=required" />
<div id="style-tags-wrapper">
<link
class="global-css"
rel="stylesheet"
href="./global.css?param=required"
/>
</div>
<script type="module" src="./hmr.ts"></script>
<style>
.import-image {
Expand All @@ -16,4 +22,6 @@
<div class="custom-communication"></div>
<div class="css-prev"></div>
<div class="css-post"></div>
<div class="link-tag-added">no</div>
<div class="link-tag-removed">no</div>
<div class="import-image"></div>

0 comments on commit 0e5c009

Please sign in to comment.