feat(nuxt): allow 'lazy' (non-blocking) server components (#21918)

This commit is contained in:
Daniel Roe 2023-07-31 09:51:09 +01:00 committed by GitHub
parent 0991e885fd
commit 5926bbeff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 84 additions and 7 deletions

View File

@ -29,6 +29,7 @@ export default defineComponent({
type: String,
required: true
},
lazy: Boolean,
props: {
type: Object,
default: () => undefined
@ -66,7 +67,7 @@ export default defineComponent({
}
}
const ssrHTML = ref('<div></div>')
const ssrHTML = ref<string>('')
if (process.client) {
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
if (renderedHTML && nuxtApp.isHydrating) {
@ -79,7 +80,7 @@ export default defineComponent({
}
})
}
ssrHTML.value = renderedHTML ?? '<div></div>'
ssrHTML.value = renderedHTML
}
const slotProps = computed(() => getSlotProps(ssrHTML.value))
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
@ -165,18 +166,19 @@ export default defineComponent({
watch(props, debounce(() => fetchComponent(), 100))
}
// TODO: allow lazy loading server islands
if (process.server || !nuxtApp.isHydrating) {
if (process.client && !nuxtApp.isHydrating && props.lazy) {
fetchComponent()
} else if (process.server || !nuxtApp.isHydrating) {
await fetchComponent()
}
return () => {
if (error.value && slots.fallback) {
if ((!html.value || error.value) && slots.fallback) {
return [slots.fallback({ error: error.value })]
}
const nodes = [createVNode(Fragment, {
key: key.value
}, [h(createStaticVNode(html.value, 1))])]
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
for (const slot in slots) {
if (availableSlots.value.includes(slot)) {

View File

@ -5,9 +5,11 @@ export const createServerComponent = (name: string) => {
return defineComponent({
name,
inheritAttrs: false,
setup (_props, { attrs, slots }) {
props: { lazy: Boolean },
setup (props, { attrs, slots }) {
return () => h(NuxtIsland, {
name,
lazy: props.lazy,
props: attrs
}, slots)
}

View File

@ -1419,6 +1419,43 @@ describe('server components/islands', () => {
await page.close()
})
it('lazy server components', async () => {
const page = await createPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle')
await page.getByText('Go to page with lazy server component').click()
const text = await page.innerText('pre')
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"> Loading server component </section><section id=\\"no-fallback\\"><div></div></section>"')
expect(text).not.toContain('async component that was very long')
expect(text).toContain('Loading server component')
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.close()
})
it('non-lazy server components', async () => {
const page = await createPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle')
await page.getByText('Go to page without lazy server component').click()
const text = await page.innerText('pre')
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"><div nuxt-ssr-component-uid=\\"0\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section><section id=\\"no-fallback\\"><div nuxt-ssr-component-uid=\\"1\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section>"')
expect(text).toContain('async component that was very long')
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.close()
})
it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => {
// @ts-expect-error ssssh! untyped secret property
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
const page = ref<HTMLDivElement | undefined>()
const mountedHTML = ref()
onMounted(() => {
mountedHTML.value = page.value?.innerHTML
})
const lazy = useRoute().query.lazy === 'true'
</script>
<template>
<div ref="page" class="end-page">
End page
<pre>{{ mountedHTML }}</pre>
<section id="fallback">
<AsyncServerComponent :lazy="lazy" :count="42">
<template #fallback>
Loading server component
</template>
</AsyncServerComponent>
</section>
<section id="no-fallback">
<AsyncServerComponent :lazy="lazy" :count="42" />
</section>
</div>
</template>

View File

@ -0,0 +1,10 @@
<template>
<div>
<NuxtLink to="/server-components/lazy/end?lazy=true">
Go to page with lazy server component
</NuxtLink>
<NuxtLink to="/server-components/lazy/end?lazy=false">
Go to page without lazy server component
</NuxtLink>
</div>
</template>