8000 feat(nuxt): allow 'lazy' (non-blocking) server components (#21918) · nuxt/nuxt@5926bbe · GitHub
[go: up one dir, main page]

Skip to content

Commit 5926bbe

Browse files
authored
feat(nuxt): allow 'lazy' (non-blocking) server components (#21918)
1 parent 0991e88 commit 5926bbe

File tree

5 files changed

+84
-7
lines changed

5 files changed

+84
-7
lines changed

packages/nuxt/src/app/components/nuxt-island.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default defineComponent({
2929
type: String,
3030
required: true
3131
},
32+
lazy: Boolean,
3233
props: {
3334
type: Object,
3435
default: () => undefined
@@ -66,7 +67,7 @@ export default defineComponent({
6667
}
6768
}
6869

69-
const ssrHTML = ref('<div></div>')
70+
const ssrHTML = ref<string>('')
7071
if (process.client) {
7172
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
7273
if (renderedHTML && nuxtApp.isHydrating) {
@@ -79,7 +80,7 @@ export default defineComponent({
7980
}
8081
})
8182
}
82-
ssrHTML.value = renderedHTML ?? '<div></div>'
83+
ssrHTML.value = renderedHTML
8384
}
8485
const slotProps = computed(() => getSlotProps(ssrHTML.value))
8586
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
@@ -165,18 +166,19 @@ export default defineComponent({
165166
watch(props, debounce(() => fetchComponent(), 100))
166167
}
167168

168-
// TODO: allow lazy loading server islands
169-
if (process.server || !nuxtApp.isHydrating) {
169+
if (process.client && !nuxtApp.isHydrating && props.lazy) {
170+
fetchComponent()
171+
} else if (process.server || !nuxtApp.isHydrating) {
170172
await fetchComponent()
171173
}
172174

173175
return () => {
174-
if (error.value && slots.fallback) {
176+
if ((!html.value || error.value) && slots.fallback) {
175177
return [slots.fallback({ error: error.value })]
176178
}
177179
const nodes = [createVNode(Fragment, {
178180
key: key.value
179-
}, [h(createStaticVNode(html.value, 1))])]
181+
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
180182
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
181183
for (const slot in slots) {
182184
if (availableSlots.value.includes(slot)) {

packages/nuxt/src/components/runtime/server-component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ export const createServerComponent = (name: string) => {
55
return defineComponent({
66
name,
77
inheritAttrs: false,
8-
setup (_props, { attrs, slots }) {
8+
props: { lazy: Boolean },
9+
setup (props, { attrs, slots }) {
910
return () => h(NuxtIsland, {
1011
name,
12+
lazy: props.lazy,
1113
props: attrs
1214
}, slots)
1315
}

test/basic.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,43 @@ describe('server components/islands', () => {
14191419
await page.close()
14201420
})
14211421

1422+
it('lazy server components', async () => {
1423+
const page = await createPage('/server-components/lazy/start')
1424+
await page.waitForLoadState('networkidle')
1425+
await page.getByText('Go to page with lazy server component').click()
1426+
1427+
const text = await page.innerText('pre')
1428+
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"> Loading server component </section><section id=\\"no-fallback\\"><div></div></section>"')
1429+
expect(text).not.toContain('async component that was very long')
1430+
expect(text).toContain('Loading server component')
1431+
1432+
// Wait for all pending micro ticks to be cleared
1433+
// await page.waitForLoadState('networkidle')
1434+
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
1435+
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
1436+
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
1437+
1438+
await page.close()
1439+
})
1440+
1441+
it('non-lazy server components', async () => {
1442+
const page = await createPage('/server-components/lazy/start')
1443+
await page.waitForLoadState('networkidle')
1444+
await page.getByText('Go to page without lazy server component').click()
1445+
1446+
const text = await page.innerText('pre')
1447+
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>"')
1448+
expect(text).toContain('async component that was very long')
1449+
1450+
// Wait for all pending micro ticks to be cleared
1451+
// await page.waitForLoadState('networkidle')
1452+
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
1453+
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
1454+
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
1455+
1456+
await page.close()
1457+
})
1458+
14221459
it.skipIf F438 (isDev)('should allow server-only components to set prerender hints', async () => {
14231460
// @ts-expect-error ssssh! untyped secret property
14241461
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
const page = ref<HTMLDivElement | undefined>()
3+
const mountedHTML = ref()
4+
onMounted(() => {
5+
mountedHTML.value = page.value?.innerHTML
6+
})
7+
8+
const lazy = useRoute().query.lazy === 'true'
9+
</script>
10+
11+
<template>
12+
<div ref="page" class="end-page">
13+
End page
14+
<pre>{{ mountedHTML }}</pre>
15+
<section id="fallback">
16+
<AsyncServerComponent :lazy="lazy" :count="42">
17+
<template #fallback>
18+
Loading server component
19+
</template>
20+
</AsyncServerComponent>
21+
</section>
22+
<section id="no-fallback">
23+
<AsyncServerComponent :lazy="lazy" :count="42" />
24+
</section>
25+
</div>
26+
</template>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<template>
2+
<div>
3+
<NuxtLink to="/server-components/lazy/end?lazy=true">
4+
Go to page with lazy server component
5+
</NuxtLink>
6+
<NuxtLink to="/server-components/lazy/end?lazy=false">
7+
Go to page without lazy server component
8+
</NuxtLink>
9+
</div>
10+
</template>

0 commit comments

Comments
 (0)
0