refactor: avoid conflicting renders

This refactors SSR handling with the existing approach, given the SSR limitation of establishing rendering separation.

This also simplifies SSR renders using the same data and adds early returns wherever possible to internally minimize delayed hydration dependency
This commit is contained in:
tbitw2549 2024-09-13 12:04:49 +03:00
parent 74231878ea
commit fb6d4a518a
4 changed files with 57 additions and 60 deletions

View File

@ -209,6 +209,10 @@ If you would like to override the default hydration triggers when dealing with d
<LazyIfMyComponent :hydrate="someCondition" />
</div>
</template>
<script setup lang="ts">
const someCondition = ref(true)
</script>
```
::read-more{to="/docs/api/utils/create-idle-loader"}
@ -225,9 +229,21 @@ Read more about using `LazyMedia` components and the accepted values.
::
::important
Nuxt will respect your component names, which means even if your components begin with a reserved prefix like Visible/Idle/Event they will not have delayed hydration. This is made to ensure you have full control over all your components and prevent breaking dynamic imports for those components. This also means you would need to explicitly add the prefix to those components. For example, if you have a component named `IdleBar`, you would need to use it like `<LazyIdleIdleBar>` and not `<LazyIdleBar>` to make it a delayed hydration component, otherwise it would be treated as a regular [dynamic import](/docs/guide/directory-structure/components#dynamic-imports)
If your components begin with a reserved delayed hydration prefix like Visible/Idle/Event, they will not have delayed hydration by default. This is made to ensure you have full control over all your components and prevent breaking dynamic imports for those components.
This also means you would need to explicitly add the prefix to those components for if you'd like for them to have delayed hydration.
For example, if you have a component named `IdleBar` and you'd like it to be delayed based on network idle time, you would need to use it like `<LazyIdleIdleBar>` and not `<LazyIdleBar>` to make it a delayed hydration component. Otherwise, it would be treated as a regular [dynamic import](/docs/guide/directory-structure/components#dynamic-imports)
::
### Caveats and best practices
Delayed hydration has many performance benefits, but in order to gain the most out of it, it's important to use it correctly:
1. Do not use `LazyVisible` for in-viewport content - `LazyVisible` is best for content that is not immediately visible and requires scrolling to get to. If it is present on screen immediately, using it as a normal component would provide better performance and loading times.
2. Do not use `LazyIf` for components that are always/never going to be hydrated - you can use a regular component/`LazyNever` respectively, which would provide better performance for each use case. Keep `LazyIf` for components that could get hydrated, but might not get hydrated immediately (for example, following user interaction).
## Direct Imports
You can also explicitly import components from `#components` if you want or need to bypass Nuxt's auto-importing functionality.

View File

@ -37,7 +37,7 @@ export const createIdleLoader = (timeout: number) => timeout
export const createVisibleLoader = (opts: Partial<IntersectionObserverInit>) => opts
/**
* A utility used to determine which events should trigger hydration in components with event-based delayed hydration.
* @param events an array of events that will be used to trigger the hydration
* A utility used to determine which event/events should trigger hydration in components with event-based delayed hydration.
* @param events an event or array of events that will be used to trigger the hydration
*/
export const createEventLoader = (events: Array<keyof HTMLElementEventMap>) => events
export const createEventLoader = (events: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>) => events

View File

@ -1,21 +1,15 @@
import { defineAsyncComponent, defineComponent, getCurrentInstance, h, hydrateOnIdle, hydrateOnInteraction, hydrateOnMediaQuery, hydrateOnVisible, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { defineAsyncComponent, defineComponent, h, hydrateOnIdle, hydrateOnInteraction, hydrateOnMediaQuery, hydrateOnVisible, mergeProps, ref, watch } from 'vue'
import type { AsyncComponentLoader, HydrationStrategy } from 'vue'
import { onNuxtReady, useNuxtApp } from '#app'
/* @__NO_SIDE_EFFECTS__ */
export const createLazyIOComponent = (loader: AsyncComponentLoader) => {
return defineComponent({
inheritAttrs: false,
setup (_, { attrs }) {
if (import.meta.server) {
return () => h(defineAsyncComponent(loader), attrs)
}
const ready = ref(false)
const nuxt = useNuxtApp()
const instance = getCurrentInstance()!
onNuxtReady(() => ready.value = true)
// This is a hack to prevent hydration mismatches for all hydration strategies
return () => ready.value ? h(defineAsyncComponent({ loader, hydrate: hydrateOnVisible(attrs.hydrate as IntersectionObserverInit | undefined) })) : nuxt.isHydrating && instance.vnode.el ? h('div', attrs) : null
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnVisible(attrs.hydrate as IntersectionObserverInit | undefined) })
const merged = mergeProps(attrs, { 'data-allow-mismatch': '' })
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
return () => h(comp, merged)
},
})
}
@ -25,33 +19,29 @@ export const createLazyNetworkComponent = (loader: AsyncComponentLoader) => {
return defineComponent({
inheritAttrs: false,
setup (_, { attrs }) {
if (import.meta.server) {
return () => h(defineAsyncComponent(loader), attrs)
const merged = mergeProps(attrs, { 'data-allow-mismatch': '' })
if (attrs.hydrate === 0) {
const comp = defineAsyncComponent(loader)
return () => h(comp, merged)
}
const ready = ref(false)
const nuxt = useNuxtApp()
const instance = getCurrentInstance()!
onNuxtReady(() => ready.value = true)
// This one seems to work fine due to the intended use case
return () => ready.value ? h(defineAsyncComponent({ loader, hydrate: hydrateOnIdle(attrs.hydrate as number | undefined) })) : nuxt.isHydrating && instance.vnode.el ? h('div', attrs) : null
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnIdle(attrs.hydrate as number | undefined) })
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
return () => h(comp, merged)
},
})
}
type HTMLEvent = keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>
/* @__NO_SIDE_EFFECTS__ */
export const createLazyEventComponent = (loader: AsyncComponentLoader) => {
return defineComponent({
inheritAttrs: false,
setup (_, { attrs }) {
if (import.meta.server) {
return () => h(defineAsyncComponent(loader), attrs)
}
const ready = ref(false)
const nuxt = useNuxtApp()
const instance = getCurrentInstance()!
onNuxtReady(() => ready.value = true)
const events: Array<keyof HTMLElementEventMap> = attrs.hydrate as Array<keyof HTMLElementEventMap> ?? ['mouseover']
return () => ready.value ? h(defineAsyncComponent({ loader, hydrate: hydrateOnInteraction(events) })) : nuxt.isHydrating && instance.vnode.el ? h('div', attrs) : null
const events: HTMLEvent = attrs.hydrate as HTMLEvent ?? 'mouseover'
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnInteraction(events) })
const merged = mergeProps(attrs, { 'data-allow-mismatch': '' })
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
return () => h(comp, merged)
},
})
}
@ -61,15 +51,11 @@ export const createLazyMediaComponent = (loader: AsyncComponentLoader) => {
return defineComponent({
inheritAttrs: false,
setup (_, { attrs }) {
if (import.meta.server) {
return () => h(defineAsyncComponent(loader), attrs)
}
const ready = ref(false)
const nuxt = useNuxtApp()
const instance = getCurrentInstance()!
onNuxtReady(() => ready.value = true)
// This one, unlike others, can cause a hydration mismatch even a whole minute after the page loads. Given a query of min-width: 1200px, with a small window, the moment the window expands to at least 1200 it hydrates and causes a hydration mismatch.
return () => ready.value ? h(defineAsyncComponent({ loader, hydrate: hydrateOnMediaQuery(attrs.hydrate ?? '(min-width: 1px)') })) : nuxt.isHydrating && instance.vnode.el ? h('div', attrs) : null
const mediaQuery = attrs.hydrate as string ?? '(min-width: 1px)'
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnMediaQuery(mediaQuery) })
const merged = mergeProps(attrs, { 'data-allow-mismatch': '' })
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
return () => h(comp, merged)
},
})
}
@ -79,24 +65,19 @@ export const createLazyIfComponent = (loader: AsyncComponentLoader) => {
return defineComponent({
inheritAttrs: false,
setup (_, { attrs }) {
if (import.meta.server) {
return () => h(defineAsyncComponent(loader), attrs)
}
const ready = ref(false)
const nuxt = useNuxtApp()
const instance = getCurrentInstance()!
onNuxtReady(() => ready.value = true)
const shouldHydrate = ref(!!(attrs.hydrate ?? true))
const strategy: HydrationStrategy = (hydrate) => {
if (!shouldHydrate.value) {
const unwatch = watch(shouldHydrate, () => hydrate(), { once: true })
return () => unwatch()
}
hydrate()
return () => {}
if (shouldHydrate.value) {
const comp = defineAsyncComponent(loader)
const merged = mergeProps(attrs, { 'data-allow-mismatch': '' })
return () => h(comp, merged)
}
const strategy: HydrationStrategy = (hydrate) => {
const unwatch = watch(shouldHydrate, () => hydrate(), { once: true })
return () => unwatch()
}
const comp = defineAsyncComponent({ loader, hydrate: strategy })
// This one seems to work fine whenever the hydration condition is achieved at client side. For example, a hydration condition of a ref greater than 2 with a button to increment causes no hydration mismatch after 3 presses of the button.
return () => ready.value ? h(defineAsyncComponent({ loader, hydrate: strategy })) : nuxt.isHydrating && instance.vnode.el ? h('div', attrs) : null
return () => h(comp, attrs)
},
})
}

View File

@ -123,9 +123,9 @@ ${nuxt.options.experimental.componentIslands ? islandType : ''}
interface _GlobalComponents {
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyIdle${pascalName}': ${type} & DefineComponent<{hydrate?: IdleRequestOptions}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyIdle${pascalName}': ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyVisible${pascalName}': ${type} & DefineComponent<{hydrate?: Partial<IntersectionObserverInit>}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyEvent${pascalName}': ${type} & DefineComponent<{hydrate?: Array<keyof HTMLElementEventMap>}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyEvent${pascalName}': ${type} & DefineComponent<{hydrate?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyMedia${pascalName}': ${type} & DefineComponent<{hydrate?: string}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyIf${pascalName}': ${type} & DefineComponent<{hydrate?: unknown}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'LazyNever${pascalName}': ${type}`).join('\n')}
@ -137,9 +137,9 @@ declare module 'vue' {
${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const Lazy${pascalName}: ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyIdle${pascalName}: ${type} & DefineComponent<{hydrate?: IdleRequestOptions}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyIdle${pascalName}: ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyVisible${pascalName}: ${type} & DefineComponent<{hydrate?: Partial<IntersectionObserverInit>}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyEvent${pascalName}: ${type} & DefineComponent<{hydrate?: Array<keyof HTMLElementEventMap>}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyEvent${pascalName}: ${type} & DefineComponent<{hydrate?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyMedia${pascalName}: ${type} & DefineComponent<{hydrate?: string}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyIf${pascalName}: ${type} & DefineComponent<{hydrate?: unknown}>`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const LazyNever${pascalName}: ${type}`).join('\n')}