Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dev): avoid FOUC when swapping out link tag (fix #7973) #8495

Merged
merged 3 commits into from Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/vite/src/client/client.ts
Expand Up @@ -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)
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
}
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>