Skip to content

Conversation

@danielroe
Copy link
Member

@danielroe danielroe commented Sep 3, 2025

🔗 Linked issue

📚 Description

this ports https://github.com/danielroe/nuxt-rebundle to an experimental feature in Nuxt. (we successfully tested it on https://nuxt.com for a while before accidentally dropping it in nuxt/nuxt.com#1795.)

This extracts handler functions from useAsyncData and useLazyAsyncData calls into separate chunks for improved code splitting and caching efficiency.

export default defineNuxtConfig({
  experimental: {
    extractAsyncDataHandlers: true
  }
})

This feature transforms inline handler functions into dynamically imported chunks:

<!-- Before -->
<script setup>
const { data } = await useAsyncData('user', async () => {
  return await heavyFunction()
})
</script>
<!-- After transformation -->
<script setup>
const { data } = await useAsyncData('user', () => 
  import('/generated-chunk.js').then(r => r.default())
)
</script>

The benefit of this transformation is that we can split out data fetching logic — while still allowing the code to be loaded if required.

Important

This feature is only recommended for static builds with payload extraction enabled, where data does not need to be re-fetched at runtime.

@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 3, 2025

Open in StackBlitz

@nuxt/kit

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

nuxt

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

@nuxt/rspack-builder

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

@nuxt/schema

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

@nuxt/vite-builder

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

@nuxt/webpack-builder

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

commit: fcb16ea

@codspeed-hq
Copy link

codspeed-hq bot commented Sep 3, 2025

CodSpeed Performance Report

Merging #33131 will not alter performance

Comparing feat/extracted-handlers (fcb16ea) with main (6cbfa2c)1

Summary

✅ 10 untouched

Footnotes

  1. No successful run was found on main (4751a6a) during the generation of this report, so 6cbfa2c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@TheAlexLichter
Copy link
Member

TheAlexLichter commented Sep 9, 2025

I've tried the latest nuxt-rebundle version and noticed that when named functions are used, extraction does not happen. This is not blocking for this PR and could be added in another iteration

Does not work

Axios for bundle size ofc.

import axios from 'axios'

const { data } = await useAsyncData('index', fetcherFn)

async function fetcherFn() {
  // Create a function that has a big bundle size
  return axios.get('/api/hello').then(res => res.data)
}

Does work

import axios from 'axios'

const { data } = await useAsyncData('index', () => axios.get('/api/hello').then(res => res.data))

@danielroe danielroe added this to the 4.2 milestone Sep 12, 2025
@danielroe danielroe marked this pull request as ready for review October 9, 2025 16:29
@coderabbitai
Copy link

coderabbitai bot commented Oct 9, 2025

Walkthrough

  • Docs: Adds experimental documentation for extractAsyncDataHandlers with config example, before/after transformation examples, and a caveat.
  • Core: Registers a new ExtractAsyncDataHandlersPlugin in the Nuxt build when experimental.extractAsyncDataHandlers is enabled (client-only, with sourcemap and rootDir).
  • Plugin: Introduces an unplugin that extracts fetcher functions from useAsyncData/useLazyAsyncData into dynamically imported chunks, collects external dependencies/imports, emits chunks with deterministic names and optional sourcemaps, and skips specified transform targets/paths.
  • Tests: Adds comprehensive tests for the plugin and updates tree-shake.test.ts to reuse a shared clean util.
  • Test utils: Adds exported clean(string?) helper.
  • Schema/types: Adds experimental.extractAsyncDataHandlers option with a resolver and a boolean entry on ConfigSchema.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly captures the primary change by indicating a new feature that extracts asyncData handlers into chunks, aligns with conventional commit style, and directly reflects the contents of the changeset without unnecessary detail or noise.
Description Check ✅ Passed The description clearly outlines the feature’s purpose, implementation details, configuration example, and transformation examples, showing a direct relationship to the changeset and demonstrating how the new experimental option works.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/extracted-handlers

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5eda40f and fcb16ea.

📒 Files selected for processing (3)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)
  • packages/nuxt/src/core/nuxt.ts (2 hunks)
  • packages/schema/src/types/schema.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/2.guide/3.going-further/1.experimental-features.md
  • packages/nuxt/src/core/nuxt.ts
  • packages/schema/src/types/schema.ts

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: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25f9282 and 19f663d.

📒 Files selected for processing (8)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)
  • packages/nuxt/src/core/nuxt.ts (2 hunks)
  • packages/nuxt/src/core/plugins/extract-async-data-handlers.ts (1 hunks)
  • packages/nuxt/test/extract-async-data-handlers.test.ts (1 hunks)
  • packages/nuxt/test/tree-shake.test.ts (1 hunks)
  • packages/nuxt/test/utils.ts (1 hunks)
  • packages/schema/src/config/experimental.ts (1 hunks)
  • packages/schema/src/types/schema.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

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

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/test/tree-shake.test.ts
  • packages/nuxt/test/utils.ts
  • packages/schema/src/config/experimental.ts
  • packages/schema/src/types/schema.ts
  • packages/nuxt/src/core/nuxt.ts
  • packages/nuxt/test/extract-async-data-handlers.test.ts
  • packages/nuxt/src/core/plugins/extract-async-data-handlers.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

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

Write unit tests for core functionality using vitest

Files:

  • packages/nuxt/test/tree-shake.test.ts
  • packages/nuxt/test/extract-async-data-handlers.test.ts
🧬 Code graph analysis (2)
packages/nuxt/src/core/nuxt.ts (2)
packages/kit/src/build.ts (1)
  • addBuildPlugin (153-165)
packages/nuxt/src/core/plugins/extract-async-data-handlers.ts (1)
  • ExtractAsyncDataHandlersPlugin (20-171)
packages/nuxt/test/extract-async-data-handlers.test.ts (2)
packages/nuxt/src/core/plugins/extract-async-data-handlers.ts (1)
  • ExtractAsyncDataHandlersPlugin (20-171)
packages/nuxt/test/utils.ts (1)
  • clean (9-18)
🪛 ast-grep (0.39.6)
packages/nuxt/test/utils.ts

[warning] 15-15: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^\\s{${indent}})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 Biome (2.1.2)
packages/nuxt/src/core/plugins/extract-async-data-handlers.ts

[error] 8-8: Do not shadow the global "Function" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

🪛 GitHub Actions: docs
packages/schema/src/types/schema.ts

[error] 1-1: Network error: Vue API fetch failed. https://vuejs.org/api/application.html#app-config

Comment on lines 75 to 156
const referencedVariables = new Set<string>()
const imports = new Set<string>()

// Walk the function body to find all identifiers
walk(fetcherFunction.body, {
scopeTracker,
enter (innerNode) {
if (innerNode.type !== 'Identifier') {
return
}

const declaration = scopeTracker.getDeclaration(innerNode.name)
if (!declaration) {
return
}

if (declaration.type === 'Import') {
// This is an imported variable, we need to include the import
imports.add(innerNode.name)
} else if (declaration.type !== 'FunctionParam') {
const functionBodyStart = fetcherFunction.body!.start
const functionBodyEnd = fetcherFunction.body!.end

// If the declaration is not within the function body, it's external
if (declaration.start < functionBodyStart || declaration.end > functionBodyEnd) {
referencedVariables.add(innerNode.name)
}
}
},
})

// Collect import statements for the referenced imports
const importStatements = new Set<string>()
walk(parseResult.program, {
enter (importDecl) {
if (importDecl.type !== 'ImportDeclaration') {
return
}

// Check if this import declaration contains any of our referenced imports
if (importDecl.specifiers?.some(spec => spec.local && imports.has(spec.local.name))) {
importStatements.add(script.slice(importDecl.start, importDecl.end))
}
},
})

const imps = Array.from(importStatements).join('\n')

// Generate a unique key for the extracted chunk
const key = `${dirname(id)}/async-data-chunk-${count++}.js`

// Get the function body content
const isBlockStatement = fetcherFunction.body.type === 'BlockStatement'

const startOffset = codeIndex + fetcherFunction.body.start
const endOffset = codeIndex + fetcherFunction.body.end

// Create the extracted chunk
const chunk = s.clone()
const parameters = [...referencedVariables].join(', ')
const returnPrefix = isBlockStatement ? '' : 'return '
const preface = `${imps}\nexport default async function (${parameters}) { ${returnPrefix}`
const suffix = ` }`

if (isBlockStatement) {
// For block statements, we need to extract the content inside the braces
chunk.overwrite(0, startOffset + 1, preface)
chunk.overwrite(endOffset - 1, code.length, suffix)
} else {
// For expression bodies, wrap in return statement
chunk.overwrite(0, startOffset, preface)
chunk.overwrite(endOffset, code.length, suffix)
}

asyncDatas[key] = {
code: chunk.toString(),
map: options.sourcemap ? chunk.generateMap({ hires: true }) : undefined,
}

// Replace the original function with a dynamic import
const importCall = `() => import('${key}').then(r => (r.default || r)(${parameters}))`
s.overwrite(codeIndex + fetcherFunction.start, codeIndex + fetcherFunction.end, importCall)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Avoid capturing property identifiers (breaks destructuring destructure cases)

For code like:

const { data } = await useAsyncData('key', async () => {
  const response = await $fetch('/api/data')
  return response.data
})

the transform currently becomes:

const { data } = await useAsyncData('key', () =>
  import('…/async-data-chunk-0.js').then(r => (r.default || r)(data))
)

Because the walker treats the property name data in response.data as a free identifier, we add it to referencedVariables and pass it into the chunk. Evaluating that argument reads the outer data binding while it is still inside the TDZ, so the call throws ReferenceError: Cannot access 'data' before initialization.

Please ignore identifiers that are acting as property keys (e.g. MemberExpression with computed === false, object literal keys, etc.) before calling scopeTracker.getDeclaration, so we only capture true free variables. Once updated, add a regression test ensuring the transform returns () => import(...).then(r => (r.default || r)()) for the snippet above.

🤖 Prompt for AI Agents
In packages/nuxt/src/core/plugins/extract-async-data-handlers.ts around lines 75
to 156, the identifier walker currently treats property names (e.g. the "data"
in response.data or object literal keys) as free identifiers and captures them,
causing TDZ ReferenceErrors; modify the enter handler to first inspect the
identifier's parent and skip calling scopeTracker.getDeclaration for identifiers
that are property keys (e.g. parent.type === 'MemberExpression' &&
parent.property === innerNode && parent.computed === false, parent.type ===
'Property' && parent.key === innerNode && parent.computed === false, class
MethodDefinition keys, and similar non-computed key positions), only then
proceed to call scopeTracker.getDeclaration for true variable references; after
the change add a regression test for the snippet in the comment asserting the
transform produces () => import(...).then(r => (r.default || r)()) (i.e., no
captured parameters).

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: 0

🧹 Nitpick comments (1)
packages/nuxt/src/core/plugins/extract-async-data-handlers.ts (1)

8-8: Rename imported type to avoid shadowing global.

The Function type import shadows the built-in global Function constructor, which can cause confusion when reading the code. Consider using a type alias to avoid this.

As per static analysis hints.

Apply this diff to resolve the shadowing:

-import type { ArrowFunctionExpression, Function } from 'oxc-parser'
+import type { ArrowFunctionExpression, Function as ASTFunction } from 'oxc-parser'

Then update the usage on line 67:

-const fetcherFunction = callExpression.arguments.find((fn): fn is Function | ArrowFunctionExpression => fn.type === 'ArrowFunctionExpression' || fn.type === 'FunctionExpression')
+const fetcherFunction = callExpression.arguments.find((fn): fn is ASTFunction | ArrowFunctionExpression => fn.type === 'ArrowFunctionExpression' || fn.type === 'FunctionExpression')

And on line 69:

-if (!fetcherFunction || (fetcherFunction.type !== 'ArrowFunctionExpression' && fetcherFunction.type !== 'FunctionExpression') || !fetcherFunction.body) {
+if (!fetcherFunction || (fetcherFunction.type !== 'ArrowFunctionExpression' && fetcherFunction.type !== 'FunctionExpression') || !fetcherFunction.body) {

Note: Line 69 check remains unchanged as it uses string literals, not the type.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19f663d and 5eda40f.

📒 Files selected for processing (3)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)
  • packages/nuxt/src/core/nuxt.ts (2 hunks)
  • packages/nuxt/src/core/plugins/extract-async-data-handlers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/nuxt/src/core/nuxt.ts
  • docs/2.guide/3.going-further/1.experimental-features.md
🧰 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/core/plugins/extract-async-data-handlers.ts
🪛 Biome (2.1.2)
packages/nuxt/src/core/plugins/extract-async-data-handlers.ts

[error] 8-8: Do not shadow the global "Function" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

⏰ 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). (13)
  • GitHub Check: test-fixtures (windows-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, 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 (windows-latest, built, vite, async, manifest-on, 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: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: test-benchmark
  • GitHub Check: test-size
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: code
🔇 Additional comments (4)
packages/nuxt/src/core/plugins/extract-async-data-handlers.ts (4)

86-104: Property identifier filtering correctly implemented.

The fix for the TDZ issue raised in the previous review has been properly implemented. The code now correctly skips identifiers that act as property keys (member expressions, object literal keys, method definitions, and property definitions) before checking if they are free variables. This prevents incorrect capture of variables such as data in response.data when the outer scope has a destructured data binding.


106-122: Dependency analysis logic is sound.

The distinction between imports, function parameters, and external variables is handled correctly:

  • Imported identifiers are collected for re-injection into the chunk
  • Function parameters are excluded (as they're locally scoped)
  • External variables (declared outside the function body) are identified by comparing declaration positions with function body boundaries

147-167: Chunk extraction logic handles both function body types correctly.

The implementation properly distinguishes between block statements and expression bodies:

  • Block statements: Extracts content inside braces
  • Expression bodies: Wraps in return statement

The use of MagicString.clone() correctly preserves the original transformation state whilst creating independent chunks.


174-176: Dynamic import replacement is correctly structured.

The replacement correctly wraps the extracted chunk in a dynamic import:

  • Returns an arrow function for lazy evaluation
  • Passes referenced variables as arguments to match the extracted function's parameters
  • Uses r.default || r as a defensive fallback (though default should always exist given the export structure)

@danielroe danielroe merged commit d083e06 into main Oct 10, 2025
47 of 49 checks passed
@danielroe danielroe deleted the feat/extracted-handlers branch October 10, 2025 03:07
@github-actions github-actions bot mentioned this pull request Oct 9, 2025
@github-actions github-actions bot mentioned this pull request Oct 23, 2025
@github-actions github-actions bot mentioned this pull request Oct 25, 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.

3 participants