Skip to content

Commit 01fa807

Browse files
timacdonaldpatak-dev
authored andcommitted
fix(dev): avoid FOUC when swapping out link tag (fix #7973) (#8495)
1 parent ab7dc1c commit 01fa807

File tree

4 files changed

+71
-8
lines changed

4 files changed

+71
-8
lines changed

packages/playground/hmr/__tests__/hmr.spec.ts

+14
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,20 @@ if (!isBuild) {
161161
expect(textpost).not.toMatch('direct')
162162
})
163163

164+
test('it swaps out link tags', async () => {
165+
await page.goto(viteTestUrl)
166+
167+
editFile('global.css', (code) => code.replace('white', 'tomato'))
168+
169+
let el = await page.$('.link-tag-added')
170+
await untilUpdated(() => el.textContent(), 'yes')
171+
172+
el = await page.$('.link-tag-removed')
173+
await untilUpdated(() => el.textContent(), 'yes')
174+
175+
expect((await page.$$('link')).length).toBe(1)
176+
})
177+
164178
test('not loaded dynamic import', async () => {
165179
await page.goto(viteTestUrl + '/counter/index.html')
166180

packages/playground/hmr/hmr.ts

+36-6
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,42 @@ if (import.meta.hot) {
4141
update.type === 'css-update' && update.path.match('global.css')
4242
)
4343
if (cssUpdate) {
44-
const el = document.querySelector('#global-css') as HTMLLinkElement
45-
text('.css-prev', el.href)
46-
// We don't have a vite:afterUpdate event, but updates are currently sync
47-
setTimeout(() => {
48-
text('.css-post', el.href)
49-
}, 0)
44+
text(
45+
'.css-prev',
46+
(document.querySelector('.global-css') as HTMLLinkElement).href
47+
)
48+
49+
// We don't have a vite:afterUpdate event.
50+
// We need to wait until the tag has been swapped out, which
51+
// includes the time taken to download and parse the new stylesheet.
52+
const observer = new MutationObserver((mutations) => {
53+
mutations.forEach((mutation) => {
54+
mutation.addedNodes.forEach((node) => {
55+
if (
56+
node.nodeType === Node.ELEMENT_NODE &&
57+
(node as Element).tagName === 'LINK'
58+
) {
59+
text('.link-tag-added', 'yes')
60+
}
61+
})
62+
mutation.removedNodes.forEach((node) => {
63+
if (
64+
node.nodeType === Node.ELEMENT_NODE &&
65+
(node as Element).tagName === 'LINK'
66+
) {
67+
text('.link-tag-removed', 'yes')
68+
text(
69+
'.css-post',
70+
(document.querySelector('.global-css') as HTMLLinkElement).href
71+
)
72+
}
73+
})
74+
})
75+
})
76+
77+
observer.observe(document.querySelector('#style-tags-wrapper'), {
78+
childList: true
79+
})
5080
}
5181
})
5282

packages/playground/hmr/index.html

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
<link id="global-css" rel="stylesheet" href="./global.css?param=required" />
1+
<div id="style-tags-wrapper">
2+
<link
3+
class="global-css"
4+
rel="stylesheet"
5+
href="./global.css?param=required"
6+
/>
7+
</div>
28
<script type="module" src="./hmr.ts"></script>
39
<style>
410
.import-image {
@@ -16,4 +22,6 @@
1622
<div class="custom-communication"></div>
1723
<div class="css-prev"></div>
1824
<div class="css-post"></div>
25+
<div class="link-tag-added">no</div>
26+
<div class="link-tag-removed">no</div>
1927
<div class="import-image"></div>

packages/vite/src/client/client.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,18 @@ async function handleMessage(payload: HMRPayload) {
8787
const newPath = `${base}${searchUrl.slice(1)}${
8888
searchUrl.includes('?') ? '&' : '?'
8989
}t=${timestamp}`
90-
el.href = new URL(newPath, el.href).href
90+
91+
// rather than swapping the href on the existing tag, we will
92+
// create a new link tag. Once the new stylesheet has loaded we
93+
// will remove the existing link tag. This removes a Flash Of
94+
// Unstyled Content that can occur when swapping out the tag href
95+
// directly, as the new stylesheet has not yet been loaded.
96+
const newLinkTag = el.cloneNode() as HTMLLinkElement
97+
newLinkTag.href = new URL(newPath, el.href).href
98+
const removeOldEl = () => el.remove()
99+
newLinkTag.addEventListener('load', removeOldEl)
100+
newLinkTag.addEventListener('error', removeOldEl)
101+
el.after(newLinkTag)
91102
}
92103
console.log(`[vite] css hot updated: ${searchUrl}`)
93104
}

0 commit comments

Comments
 (0)