Skip to content

Conversation

@danielroe
Copy link
Member

🔗 Linked issue

📚 Description

previously we were always scope-tracking every <script> block. this changes this to do so only if there's a template with lazy hydration attributes present.

I also move magic string creation + scope tracking a bit later to save some work if there's an error parsing the <template>

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 11, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33213

nuxt

npm i https://pkg.pr.new/nuxt@33213

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33213

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33213

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33213

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33213

commit: d1c59a3

@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

Walkthrough

The transform now runs only when a new template-matching regex detects likely lazy-hydration usage. Template parsing precedes script analysis; scope tracking is introduced via ScopeTracker and parseAndWalk to avoid rewriting in-scope declarations. MagicString is initialised after template parsing. The transformation iterates template AST nodes, detects hydration strategies from attributes and auto-imported components, and rewrites tags to Lazy with precise location updates for both opening and closing tags. If a strategy exists but the element is not a Lazy component, a dev/test warning is logged and the node is skipped. Error handling remains, with change detection moved inside the main try.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly signals a performance change in Nuxt to avoid running the lazy hydration transform unnecessarily, aligning with the PR’s intent to skip scope-tracking and defer magic-string creation. It is concise, focused on the main change, and free of irrelevant noise. The phrase "with filter" is slightly ambiguous and could be clarified for readers scanning history.
Description Check ✅ Passed The description clearly describes the core changes: scope-tracking is no longer run for every <script> block and magic-string creation is deferred until after template parsing, which matches the provided raw_summary and PR objectives. It is directly related to the changeset and not off-topic. For completeness, the author could add a linked issue number and a short note about tests or performance benchmarks.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/lazy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/nuxt/src/components/plugins/lazy-hydration-transform.ts (2)

91-100: Support v-bind: prefix for strategy props

Templates may use v-bind:hydrate-on-idle. Only : is handled now.

- for (const attr in node.attributes) {
-   const isDynamic = attr.startsWith(':')
-   const prop = camelCase(isDynamic ? attr.slice(1) : attr)
+ for (const attr in node.attributes) {
+   const isDynamic = attr.startsWith(':') || attr.startsWith('v-bind:')
+   const raw = isDynamic ? attr.replace(/^v-bind:|^:/, '') : attr
+   const prop = camelCase(raw)

116-123: Guard against failed tag matches to prevent runtime errors

startingChunk! assumes a match; nested same-named tags or unusual whitespace could break it.

- const { 0: startingChunk, index: startingPoint = 0 } = chunk.match(new RegExp(`<${node.name}[^>]*>`)) || {}
- s.overwrite(startingPoint + chunkOffset, startingPoint + chunkOffset + startingChunk!.length, startingChunk!.replace(node.name, newName))
+ const mOpen = chunk.match(new RegExp(`<${node.name}[^>]*>`))
+ if (!mOpen) { return }
+ const { 0: startingChunk, index: startingPoint = 0 } = mOpen
+ (s ??= new MagicString(code)).overwrite(
+   startingPoint + chunkOffset,
+   startingPoint + chunkOffset + startingChunk.length,
+   startingChunk.replace(node.name, newName)
+ )
🧹 Nitpick comments (2)
packages/nuxt/src/components/plugins/lazy-hydration-transform.ts (2)

71-71: Lazily instantiate MagicString to avoid unnecessary allocations

Create MagicString only when the first overwrite occurs.

- const s = new MagicString(code)
+ let s: MagicString | undefined
- s.overwrite(startingPoint + chunkOffset, startingPoint + chunkOffset + startingChunk!.length, startingChunk!.replace(node.name, newName))
+ (s ??= new MagicString(code)).overwrite(
+   startingPoint + chunkOffset,
+   startingPoint + chunkOffset + startingChunk!.length,
+   startingChunk!.replace(node.name, newName)
+ )
- if (endingChunk && endingPoint) {
-   s.overwrite(endingPoint + chunkOffset, endingPoint + chunkOffset + endingChunk.length, endingChunk.replace(node.name, newName))
+ if (endingChunk && endingPoint !== undefined) {
+   (s ??= new MagicString(code)).overwrite(
+     endingPoint + chunkOffset,
+     endingPoint + chunkOffset + endingChunk.length,
+     endingChunk.replace(node.name, newName)
+   )
- if (s.hasChanged()) {
+ if (s?.hasChanged()) {

Also applies to: 117-118, 121-122, 125-132


103-111: Consider deduping per-file warning spam

The warning for non-lazy components with lazy props could flood logs in large templates. Gate by a Set per file.

// suggestion outside changed hunk
const warned = new Set<string>()
// ...
const key = `${id}:${node.name}`
if (!warned.has(key)) {
  warned.add(key)
  logger.warn(/* existing message */)
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 327ba8f and d1c59a3.

📒 Files selected for processing (1)
  • packages/nuxt/src/components/plugins/lazy-hydration-transform.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/components/plugins/lazy-hydration-transform.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: test-size
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: test-benchmark
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: code
🔇 Additional comments (1)
packages/nuxt/src/components/plugins/lazy-hydration-transform.ts (1)

63-69: Double-check oxc parser handling for <script setup lang="ts">

Passing a .vue filename may prevent correct TS/JSX parsing. Verify parseAndWalk covers both classic and setup scripts with TS.

Would you like a quick repo script to scan for <script setup lang="ts"> usage and run a small fixture through this transform?

Comment on lines +63 to +69
const scopeTracker = new ScopeTracker({ preserveExitedScopes: true })
for (const { 0: script } of code.matchAll(SCRIPT_RE)) {
if (!script) { continue }
try {
parseAndWalk(script, id, { scopeTracker })
} catch { /* ignore */ }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Scope-collision check likely misses common casings

You compare node.name directly against declarations; Vue templates may use kebab-, Pascal- or camel-case. Normalise before checking to avoid false negatives.

- if (scopeTracker.getDeclaration(node.name)) {
-   return
- }
+ const scopeNames = new Set([
+   node.name,
+   pascalCase(node.name.replace(/^(?:Lazy|lazy-)/, '')),
+   camelCase(node.name),
+   camelCase(pascalCase(node.name.replace(/^(?:Lazy|lazy-)/, ''))),
+ ])
+ for (const n of scopeNames) {
+   if (scopeTracker.getDeclaration(n)) { return }
+ }

Also applies to: 79-81

🤖 Prompt for AI Agents
In packages/nuxt/src/components/plugins/lazy-hydration-transform.ts around lines
63-69, the scope-collision check compares node.name directly to declarations and
misses matches when casing or kebab/camel/Pascal variants are used; normalize
both sides before comparison (e.g., create a canonical form by lowercasing and
comparing both raw and hyphen-stripped/normalized variants or by converting
kebab-case to camelCase and also lowercasing) and use that canonical form in the
equality checks so kebab-, Pascal- and camel-case variants match; apply the same
normalization fix to the similar check at lines 79-81.

@codspeed-hq
Copy link

codspeed-hq bot commented Sep 11, 2025

CodSpeed Performance Report

Merging #33213 will not alter performance

Comparing perf/lazy (d1c59a3) with main (327ba8f)

Summary

✅ 10 untouched

@danielroe danielroe merged commit 13aef2a into main Sep 11, 2025
85 of 86 checks passed
@danielroe danielroe deleted the perf/lazy branch September 11, 2025 20:52
@github-actions github-actions bot mentioned this pull request Sep 11, 2025
@github-actions github-actions bot mentioned this pull request Sep 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants