-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
feat(nuxt): delayed/lazy hydration support #26468
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
Conversation
|
packages/nuxt/src/components/runtime/client-delayed-component.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
π§Ή Nitpick comments (9)
docs/2.guide/2.directory-structure/1.components.md (2)
128-129
: Consider making the wording more concise.The phrase "a lot of content and a lot of components" can sound repetitive. Try rephrasing to reduce wordiness.
π§° Tools
πͺ LanguageTool
[style] ~128-~128: The phrase βa lot ofβ might be wordy and overused. Consider using an alternative.
Context: ... pages may include a lot of content and a lot of components, and most of the time not al...(A_LOT_OF)
[style] ~129-~129: Consider a shorter alternative to avoid wordiness.
Context: ...rly can negatively impact performance. In order to optimize your app, you may want to dela...(IN_ORDER_TO_PREMIUM)
512-514
: Add a comma for clarity.Consider adding a comma before βand you canβtβ to separate the clauses and enhance readability.
- * You can't access the 'island context' from the rest of your app and you can't access ... + * You can't access the 'island context' from the rest of your app, and you can't access ...π§° Tools
πͺ LanguageTool
[uncategorized] ~514-~514: Use a comma before βandβ if it connects two independent clauses (unless they are closely connected and short).
Context: ...sland context' from the rest of your app and you can't access the context of the res...(COMMA_COMPOUND_SENTENCE)
packages/nuxt/test/component-loader.test.ts (2)
91-124
: Multiple lazy hydration strategies are well tested.The tests cover various strategies effectively. Consider including a conflicting strategy scenario to confirm fallback or prioritisation behaviour.
126-172
: Add resource cleanup for Rollup bundles.Wrap Rollup usage in a try/finally block to ensure bundles are closed, preventing potential memory leaks during repeated tests.
async function transform (code: string, filename: string) { + let bundle try { - const bundle = await rollup({ + bundle = await rollup({ input: filename, plugins: [ // ... ] }) const { output: [chunk] } = await bundle.generate({}) return chunk.code.trim() + } finally { + if (bundle) { + await bundle.close() + } } }packages/nuxt/src/components/plugins/loader.ts (2)
24-24
: Regex complexity caution.This expanded regex handles multiple modifiers. Consider extracting some handling into helper functions or composable logic for better maintainability.
86-137
: Consider splitting out each hydration strategy.The switch-case approach is straightforward but large. Extracting each strategy into a helper method would aid readability and simplify future expansions.
packages/nuxt/src/components/runtime/lazy-hydrated-component.ts (3)
15-15
: Consider handling potential undefined values more explicitly.The non-null assertion operators (
!
) suggest thatssrContext
ormodules
might be undefined in some cases, which could lead to runtime errors.- ssrContext!.modules!.delete(id) + if (ssrContext?.modules) { + ssrContext.modules.delete(id) + }
31-38
: Type casting approach could be improved for better clarity.The type casting using
as unknown as
may be necessary due to TypeScript limitations, but it reduces type safety.Consider adding a clear comment explaining the reason for this type casting approach, or if possible, use a more type-safe approach:
hydrateOnVisible: { - type: [Object, Boolean] as unknown as () => true | IntersectionObserverInit, + // Cast needed to support both boolean and IntersectionObserverInit types + type: [Object, Boolean] as unknown as () => true | IntersectionObserverInit, required: false, },
105-114
: Add a comment to explain the never-hydrate strategy.The
hydrateNever
function is a simple empty function, which effectively prevents hydration. A comment explaining this would help other developers understand the intention.- const hydrateNever = () => {} + // This function purposely does nothing, effectively preventing the component from ever hydrating + const hydrateNever = () => {}
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (4)
docs/2.guide/2.directory-structure/1.components.md
(2 hunks)packages/nuxt/src/components/plugins/loader.ts
(3 hunks)packages/nuxt/src/components/runtime/lazy-hydrated-component.ts
(1 hunks)packages/nuxt/test/component-loader.test.ts
(1 hunks)
π§° Additional context used
π§ Learnings (1)
packages/nuxt/src/components/plugins/loader.ts (1)
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-12T07:37:17.193Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.
πͺ LanguageTool
docs/2.guide/2.directory-structure/1.components.md
[style] ~128-~128: The phrase βa lot ofβ might be wordy and overused. Consider using an alternative.
Context: ... pages may include a lot of content and a lot of components, and most of the time not al...
(A_LOT_OF)
[style] ~129-~129: Consider a shorter alternative to avoid wordiness.
Context: ...rly can negatively impact performance. In order to optimize your app, you may want to dela...
(IN_ORDER_TO_PREMIUM)
[uncategorized] ~137-~137: Loose punctuation mark.
Context: ...rategy can be used per lazy component. ::warning Currently Nuxt's built-in lazy ...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~139-~139: Loose punctuation mark.
Context: ...with direct imports from #components
. :: #### hydrate-on-visible
Hydrates t...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~153-~153: Loose punctuation mark.
Context: ...on-visible /> ``` ::read-more{to="https://developer.mozilla...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~155-~155: Loose punctuation mark.
Context: ...t the options for hydrate-on-visible
. :: ::note Under the hood, this uses Vue'...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~157-~157: Loose punctuation mark.
Context: ...e options for hydrate-on-visible
. :: ::note Under the hood, this uses Vue's bu...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~159-~159: Loose punctuation mark.
Context: ...ponents/async.html#hydrate-on-visible). :: #### hydrate-on-idle
Hydrates the ...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~175-~175: Loose punctuation mark.
Context: ...te-on-idle /> ``` ::note Under the hood, this uses Vue's bu...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~177-~177: Loose punctuation mark.
Context: ...components/async.html#hydrate-on-idle). :: #### hydrate-on-interaction
Hydrat...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~193-~193: Loose punctuation mark.
Context: ...drating on pointerenter
and focus
. ::note Under the hood, this uses Vue's bu...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~195-~195: Loose punctuation mark.
Context: ...nts/async.html#hydrate-on-interaction). :: #### hydrate-on-media-query
Hydrat...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~209-~209: Loose punctuation mark.
Context: ...h: 768px)" /> ``` ::note Under the hood, this uses Vue's bu...
(UNLIKELY_OPENING_PUNCTUATION)
[uncategorized] ~211-~211: Loose punctuation mark.
Context: ...nts/async.html#hydrate-on-media-query). :: #### hydrate-after
Hydrates the co...
(UNLIKELY_OPENING_PUNCTUATION)
[style] ~288-~288: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...amount of time. * hydrate-on-idle
is for components that can be hydrated whe...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[uncategorized] ~514-~514: Use a comma before βandβ if it connects two independent clauses (unless they are closely connected and short).
Context: ...sland context' from the rest of your app and you can't access the context of the res...
(COMMA_COMPOUND_SENTENCE)
β° Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: codeql (javascript-typescript)
- GitHub Check: build
- GitHub Check: code
π Additional comments (11)
docs/2.guide/2.directory-structure/1.components.md (1)
290-290
: Clear best practice note.Advising developers to avoid
hydrate-never
on interactive components is helpful and shows good emphasis on user experience.packages/nuxt/test/component-loader.test.ts (2)
12-25
: Good test coverage for standard component resolution.These assertions confirm both normal and lazy components are resolved and transformed correctly.
59-89
: JSX test ensures broader coverage.Validating component resolution in a JSX context safeguards compatibility with different template syntaxes.
packages/nuxt/src/components/plugins/loader.ts (2)
16-18
: Validate or default the new options in LoaderOptions.
srcDir
andclientDelayedComponentRuntime
are essential for the lazy hydration workflow. Make sure they're provided or fallback accordingly if omitted.
50-53
: Fallback logic is a neat safety net.The step-by-step resolution for normal components and modifiers helps preserve backward compatibility. Just watch for naming collisions.
packages/nuxt/src/components/runtime/lazy-hydrated-component.ts (6)
5-28
: Well-designed foundation for lazy hydration components.This
defineLazyComponent
higher-order function provides an excellent abstraction for creating different types of lazy-loaded components with various hydration strategies. The function correctly handles SSR by removing the component from the SSR context to avoid generating prefetch/preload tags, and it properly encapsulates the component loading logic.I particularly like the
onVnodeMounted
event emitter, which will be useful for developers needing to respond to hydration events.
47-49
: Good handling of immediate hydration case.The conditional expression handles the special case of immediate hydration when
hydrateOnIdle
is 0, which is an important optimization.
52-63
: Excellent use of TypeScript'ssatisfies
operator.The usage of
satisfies
provides good type checking whilst preserving the literal type ofdefaultInteractionEvents
. This is a modern TypeScript pattern that ensures the events are compatible withHTMLElementEventMap
.
75-88
: Thewatch
cleanup is well-implemented.The implementation correctly returns a cleanup function that unsubscribes from the watcher when the component is unmounted, preventing memory leaks.
90-103
: Well-structured timeout handling.The implementation correctly sets up and clears the timeout, preventing memory leaks. The special case handling for immediate hydration is also appropriately implemented.
1-114
: Overall, excellent implementation of lazy hydration strategies.This file provides a comprehensive set of lazy hydration strategies that align well with the PR objective to improve performance. The code is well-structured, follows modern Vue and TypeScript practices, and handles important considerations like SSR and cleanup. The emitted 'hydrated' event provides good developer feedback.
Consider adding error handling for async component loading and documenting the type casting approach for better maintainability.
CodSpeed Performance ReportMerging #26468 will not alter performanceComparing Summary
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
π§Ή Nitpick comments (7)
test/basic.test.ts (4)
2863-2895
: Consider using explicit wait conditions to reduce flakiness
The test employs a sizable timeout and severalwaitFor...
calls. In slower CI environments, this approach might still lead to sporadic failures. Using more precise conditions or repeated polling for specific DOM states can help mitigate test timing issues.
2922-2933
: Refine timing-based tests to prevent false positives
The test sets a time-based hydration trigger and checks it in a single scenario. In resource-constrained or busy environments, timing inaccuracies could cause sporadic test results. Adding more robust checks (e.g., verifying that the timeout actually expired before hydration) would increase test reliability.
2934-2952
: Minimise loop iteration for faster test execution
The loop increments the counter from 0 to 10, which may be unnecessarily high. Reducing the iteration count or using a dedicated assertion for final state can shorten test duration and reduce potential flakiness while still ensuring correctness and reactivity validation.
2953-2966
: Consider testing consecutive hydration events
Currently, the test verifies a single hydration event by checking console logs for'Component hydrated'
. You might extend this test to cover rapid consecutive triggers or complex user interactions, ensuring the component remains stable during multiple hydration phases.packages/nuxt/src/components/runtime/lazy-hydrated-component.ts (3)
19-23
: Validate nested async components to avoid unnecessary overhead
Using two layers ofdefineAsyncComponent
may cause additional overhead or complexity if the loader is requested too soon or multiple times. Ensure that child components are not redundantly fetching resources or introducing performance bottlenecks.
82-87
: Avoid repeated re-hydration triggers
WhenhydrateWhen
becomestrue
, the watcher immediately callshydrate()
. IfhydrateWhen
toggles multiple times, there is a risk of repeated re-hydration unless carefully handled. You might add a check within the watcher to prevent multiple invocations if toggled again.
107-115
: Consider warning for conflicting hydration settings
When a developer setshydrateNever
totrue
, other hydration props (such ashydrateOnIdle
) become moot. Emitting a compile-time or runtime warning could help developers detect conflicting options more readily.
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (2)
packages/nuxt/src/components/runtime/lazy-hydrated-component.ts
(1 hunks)test/basic.test.ts
(1 hunks)
β° Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: codeql (javascript-typescript)
- GitHub Check: build
- GitHub Check: Socket Security: Pull Request Alerts
- GitHub Check: code
π Additional comments (5)
test/basic.test.ts (2)
2896-2912
: Verify coverage for multiple interactive events
You hover and then click to test delayed hydration triggers. It could be beneficial to cover multi-event scenarios (e.g., a double-click or a focus-then-click) to ensure consistency with more complex interaction patterns.
2913-2921
: Well-structured coverage for name-based hydration overrides
The test confirms that no default hydration delay is enforced when the component name matches a recognised modifier. This logic appears comprehensive. Good job!packages/nuxt/src/components/runtime/lazy-hydrated-component.ts (3)
14-16
: Guard against possible null or undefined ssrContext
The code uses non-null assertions (!
) onssrContext
and itsmodules
property. Consider optional chaining or safety checks to handle potential edge cases in development or unusual build contexts.
30-50
: Confirm clarity in idle fallback logic
The code checks forprops.hydrateOnIdle === 0
to switch to immediate hydration, which is ingenious. However, it may be worth documenting this behaviour more explicitly in the code comments to prevent confusion, as zero can be mistaken for a valid idle delay.
100-102
: Good practice clearing the timeout upon unmount
Your implementation properly usesreturn () => clearTimeout(id)
to prevent memory leaks or unexpected triggers should the component unmount prematurely. This is a robust approach.
CodSpeed Performance ReportMerging #26468 will not alter performanceComparing Summary
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
β»οΈ Duplicate comments (1)
test/basic.test.ts (1)
2923-2933
: π οΈ Refactor suggestionTime-based hydration test doesn't verify eventual hydration.
The test only verifies the initial unhydrated state, but doesn't check that the component actually gets hydrated after the specified time delay.
await page.locator('[data-testid=hydrate-after]', { hasText: unhydratedText }).waitFor({ state: 'visible' }) await page.locator('[data-testid=hydrate-after]', { hasText: hydratedText }).waitFor({ state: 'hidden' }) + + // Wait for hydration to occur after the specified time + await page.waitForFunction(() => { + return document.querySelector('[data-testid=hydrate-after]')?.textContent?.includes('This is mounted.') === true + }, { timeout: 2500 })
π§Ή Nitpick comments (2)
test/basic.test.ts (2)
2864-2895
: Test is robust, but could include more specific state assertions.The test covers different hydration strategies comprehensively, but could be more explicit about expected states. Consider adding assertions that verify state changes directly rather than just presence/absence of text.
- await page.locator('data-testid=hydrate-on-visible', { hasText: hydratedText }).waitFor() + await page.locator('data-testid=hydrate-on-visible', { hasText: hydratedText }).waitFor() + expect(await page.locator('data-testid=hydrate-on-visible').getAttribute('data-hydrated')).toBe('true')
2954-2968
: Test could use more reliable event detection.The test relies on scanning console logs for hydration events, which works but could be more reliable with more direct event detection.
const { page, consoleLogs } = await renderPage('/lazy-import-components/model-event') -const initialLogs = consoleLogs.filter(log => log.type === 'log' && log.text === 'Component hydrated') -expect(initialLogs.length).toBe(0) +// Setup event listener before triggering hydration +const hydrationPromise = page.evaluate(() => { + return new Promise(resolve => { + window.addEventListener('@hydrated', () => resolve(true), { once: true }) + }) +}) await page.getByTestId('count').click() -// Wait for all pending micro ticks to be cleared in case hydration hasn't finished yet. -await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) -const hydrationLogs = consoleLogs.filter(log => log.type === 'log' && log.text === 'Component hydrated') -expect(hydrationLogs.length).toBeGreaterThan(0) +// Wait for the hydration event with a reasonable timeout +await expect(hydrationPromise).resolves.toBe(true)
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (1)
test/basic.test.ts
(1 hunks)
β° Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: codeql (javascript-typescript)
- GitHub Check: build
- GitHub Check: code
π Additional comments (3)
test/basic.test.ts (3)
2896-2912
: Test for custom hydration triggers is well structured.This test clearly validates that custom triggers override defaults with the correct sequence of interactions and assertions.
2914-2921
: Good edge case testing for name-sensitive components.This test properly verifies that components with names that could be confused with hydration modifiers are not automatically affected by delayed hydration.
2935-2952
: Thorough reactivity testing.The test effectively validates that model binding and reactivity are preserved in lazily hydrated components through multiple interactions.
CodSpeed Performance ReportMerging #26468 will not alter performanceComparing Summary
|
CodSpeed Performance ReportMerging #26468 will not alter performanceComparing Summary
|
Co-authored-by: Daniel Roe <daniel@roe.dev>
π Linked issue
Resolves #24242
β Type of change
π Description
Lazy components are great for controlling the chunk sizes in your app, but they don't always enhance runtime performance, as they still load eagerly unless conditionally rendered. In real-world applications, some pages may include a lot of content and a lot of components, and most of the time not all of them need to be interactive as soon as the page is loaded. Having them all load eagerly can negatively impact performance.
In order to optimize your app, you may want to delay the hydration of some components until they're visible, or until the browser is done with more important tasks.
Nuxt supports this using lazy (or delayed) hydration, allowing you to control when components become interactive.
Hydration Strategies
Nuxt provides a range of built-in hydration strategies. Only one strategy can be used per lazy component.
::warning
Currently Nuxt's built-in lazy hydration only works in single-file components (SFCs), and requires you to define the prop in the template (rather than spreading an object of props via
v-bind
).::
hydrate-on-visible
Hydrates the component when it becomes visible in the viewport.
::read-more{to="https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver" title="IntersectionObserver options"}
Read more about the options for
hydrate-on-visible
.::
::note
Under the hood, this uses Vue's built-in
hydrateOnVisible
strategy.::
hydrate-on-idle
Hydrates the component when the browser is idle. This is suitable if you need the component to load as soon as possible, but not block the critical rendering path.
You can also pass a number which serves as a max timeout.
::note
Under the hood, this uses Vue's built-in
hydrateOnIdle
strategy.::
hydrate-on-interaction
Hydrates the component after a specified interaction (e.g., click, mouseover).
If you do not pass an event or list of events, it defaults to hydrating on
pointerenter
andfocus
.::note
Under the hood, this uses Vue's built-in
hydrateOnInteraction
strategy.::
hydrate-on-media-query
Hydrates the component when the window matches a media query.
::note
Under the hood, this uses Vue's built-in
hydrateOnMediaQuery
strategy.::
hydrate-after
Hydrates the component after a specified delay (in milliseconds).
hydrate-when
Hydrates the component based on a boolean condition.
hydrate-never
Never hydrates the component.
Listening to Hydration Events
All delayed hydration components emit a
@hydrated
event when they are hydrated.