diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts
index 3f8bfd3876..e2990cfed6 100644
--- a/packages/nuxt/src/app/components/nuxt-island.ts
+++ b/packages/nuxt/src/app/components/nuxt-island.ts
@@ -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('
')
+ const ssrHTML = ref('')
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 ?? ''
+ ssrHTML.value = renderedHTML
}
const slotProps = computed(() => getSlotProps(ssrHTML.value))
const uid = ref(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 || '', 1))])]
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
for (const slot in slots) {
if (availableSlots.value.includes(slot)) {
diff --git a/packages/nuxt/src/components/runtime/server-component.ts b/packages/nuxt/src/components/runtime/server-component.ts
index 4dcfa8cce5..777204209e 100644
--- a/packages/nuxt/src/components/runtime/server-component.ts
+++ b/packages/nuxt/src/components/runtime/server-component.ts
@@ -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)
}
diff --git a/test/basic.test.ts b/test/basic.test.ts
index 06968d32ab..879bc8c886 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -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 "')
+ 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 This is a .server (20ms) async component that was very long ...
42
This is a .server (20ms) async component that was very long ...
42
"')
+ 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
diff --git a/test/fixtures/basic/pages/server-components/lazy/end.vue b/test/fixtures/basic/pages/server-components/lazy/end.vue
new file mode 100644
index 0000000000..7009c1e7ac
--- /dev/null
+++ b/test/fixtures/basic/pages/server-components/lazy/end.vue
@@ -0,0 +1,26 @@
+
+
+
+
+ End page
+
{{ mountedHTML }}
+
+
+
+ Loading server component
+
+
+
+
+
+
diff --git a/test/fixtures/basic/pages/server-components/lazy/start.vue b/test/fixtures/basic/pages/server-components/lazy/start.vue
new file mode 100644
index 0000000000..a14cf6eada
--- /dev/null
+++ b/test/fixtures/basic/pages/server-components/lazy/start.vue
@@ -0,0 +1,10 @@
+
+
+
+ Go to page with lazy server component
+
+
+ Go to page without lazy server component
+
+
+