Skip to content

turbopack: Add NEEDS_RESOLVE const to reduce binary size from monomorphization#92686

Draft
lukesandberg wants to merge 3 commits intocanaryfrom
turbopack_binary_size
Draft

turbopack: Add NEEDS_RESOLVE const to reduce binary size from monomorphization#92686
lukesandberg wants to merge 3 commits intocanaryfrom
turbopack_binary_size

Conversation

@lukesandberg
Copy link
Copy Markdown
Contributor

Summary

  • Binary size analysis reports for turbopack's native binary (153MB unstripped / ~103MB stripped / ~34MB gzipped), breaking down overhead by crate, section, and monomorphization category.
  • TaskInput::NEEDS_RESOLVE const — a compile-time constant on the TaskInput trait that indicates whether a type may contain unresolved Vcs. Only Vc<T> sets it to true; compound types OR their elements' values.
  • trivial_clone_resolve — a simpler resolve functor used when NEEDS_RESOLVE is false, replacing the full async resolve_functor_impl state machine for ~60% of turbo-tasks functions whose argument tuples contain no Vc values.

Key findings from analysis

Subsystem .text size % of code
turbo-tasks infrastructure ~8.4 MiB ~12-15% of stripped binary
functor() closures ~3.5 MiB Unique per fn-item type
conditional_update closures 1.36 MiB Per-type equality comparison
resolve_functor_impl 688 KiB Per unique arg-tuple type

This PR addresses

resolve_functor_impl monomorphization: ~527 of 877 instances contain no Vc arguments and compile to trivially-different async state machines. By branching on the new NEEDS_RESOLVE const at compile time, these instances use trivial_clone_resolve instead — a simpler function body that the compiler can better deduplicate via ICF. Estimated savings: ~200-350 KiB of .text.

Design notes

  • The derive(TaskInput) macro computes NEEDS_RESOLVE at macro-expansion time via syntactic Vc detection rather than const-eval of field type NEEDS_RESOLVE. This avoids const-evaluation cycles with recursive types (e.g. ChunkGroupEntry which has Box of ChunkGroupEntry fields).
  • The if T::NEEDS_RESOLVE branch in ArgMeta::new / with_filter_trait_call is evaluated in a const fn context — both arms are bare function pointers, so this is valid.

Future opportunities identified in the reports

Proposal Estimated savings
Feature-gate ValueDebug (dbg/dbg_depth) ~2.5-3.5 MiB
Type-erase conditional_update comparison ~700-900 KiB
Eliminate duplicate dbg() turbo-task ~350-700 KiB
Enable --icf=all linker flag ~500-1,500 KiB

Test plan

  • cargo check -p turbo-tasks -p turbopack-core -p turbopack-ecmascript -p turbopack -p next-api — all compile cleanly
  • cargo test -p turbo-tasks --lib --tests — 50/50 tests pass
  • cargo fmt -- --check — no formatting issues

lukesandberg and others added 3 commits April 9, 2026 00:34
Analysis of the next-swc native binary (153MB unstripped, 103MB stripped, 34MB gzipped)
using cargo-bloat, nm symbol analysis, and dependency tree analysis. Identifies turbo-tasks
monomorphization (21.6 MiB), the Wasmer/Cranelift WASM engine (9.5 MiB), and LightningCSS
(8 MiB) as the top size contributors with actionable reduction recommendations.

Co-Authored-By: Claude <noreply@anthropic.com>
Deep-dive into per-function and per-value-type code generation overhead
from #[turbo_tasks::function] and #[turbo_tasks::value] macros. Identifies
~8.4 MiB of infrastructure glue (12-15% of stripped binary) with concrete
refactoring proposals for ~4.5-6.5 MiB savings.

Co-Authored-By: Claude <noreply@anthropic.com>
Introduces `const NEEDS_RESOLVE: bool` on the `TaskInput` trait, defaulting to
`false`. Only `Vc<T>` sets it to `true`; compound types (tuples, Vec, Option,
etc.) OR their elements' values. The `#[derive(TaskInput)]` macro computes this
at expansion time via syntactic `Vc<…>` detection to avoid const-eval cycles
with recursive types.

`ArgMeta` now branches on `T::NEEDS_RESOLVE` to select between the full
`resolve_functor_impl<T>` (async Vc resolution state machine) and a new
`trivial_clone_resolve<T>` (simple clone). For the ~60% of turbo-tasks
functions whose argument tuples contain no `Vc`, this avoids monomorphizing the
expensive async resolver, estimated to save ~200-350 KiB of .text.

Co-Authored-By: Claude <noreply@anthropic.com>
@nextjs-bot nextjs-bot added created-by: Turbopack team PRs by the Turbopack team. Turbopack Related to Turbopack with Next.js. labels Apr 12, 2026
///
/// The default is `false`. Only [`Vc<T>`][Vc] overrides this to `true`. Compound types (e.g.
/// `Vec<T>`, tuples, derived types) should OR their fields' `NEEDS_RESOLVE` values.
const NEEDS_RESOLVE: bool = false;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

is false the right default?

Comment on lines +200 to +203
let needs_resolve_impl = if any_field_needs_resolve {
quote! { true }
} else {
quote! { false }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
let needs_resolve_impl = if any_field_needs_resolve {
quote! { true }
} else {
quote! { false }
// Collect the idents of generic type parameters so we can OR in their NEEDS_RESOLVE.
// This is safe from const-eval cycles because `T::NEEDS_RESOLVE` refers to the concrete
// substitution at monomorphization time, not `Self`.
let generic_param_idents: Vec<_> = generic_params.iter().map(|p| &p.ident).collect();
let needs_resolve_impl = if any_field_needs_resolve {
quote! { true }
} else {
quote! { false #(|| <#generic_param_idents as turbo_tasks::TaskInput>::NEEDS_RESOLVE)* }

The TaskInput derive macro always emits NEEDS_RESOLVE = false for generic types, ignoring that generic type parameters could be Vc<T> at monomorphization time.

Fix on Vercel

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 12, 2026

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing turbopack_binary_size (dfa08f7) with canary (db0fdc3)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

created-by: Turbopack team PRs by the Turbopack team. Turbopack Related to Turbopack with Next.js.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants