Skip to content

perf: reduce context copying in resolver pipeline via Object.create#1085

Open
hugo-ccabral wants to merge 1 commit intomainfrom
perf/reduce-resolver-context-copying
Open

perf: reduce context copying in resolver pipeline via Object.create#1085
hugo-ccabral wants to merge 1 commit intomainfrom
perf/reduce-resolver-context-copying

Conversation

@hugo-ccabral
Copy link
Contributor

@hugo-ccabral hugo-ccabral commented Feb 25, 2026

Summary

  • Replace context spreading with Object.create() in withResolveChain(), withResolveChainOfType(), and invokeResolverWithProps()
  • These functions were spreading the entire context object (10+ fields) just to change 1-2 properties (resolveChain, monitoring, resolverId)
  • Object.create(ctx) uses prototypal inheritance — only the changed property is allocated, all others are inherited through the prototype chain
  • Also simplified the resolveId override in resolvePropsWithHints() to avoid a spread

Context

The resolver pipeline calls these functions recursively for every prop with hints. A page with 30 sections, each with nested props, generates hundreds of context copies. CPU profile showed the full resolver pipeline at ~3% CPU, and memory profile showed context/closure allocations contributing to the 70% framework allocation share.

Safety

All consumers of these context objects access known property names (ctx.resolveChain, ctx.monitoring, etc.) — prototypal inheritance works correctly for property access. No code iterates over context keys or uses Object.keys().

Test plan

  • Run existing deco test suite
  • Verify resolver chain is correctly built (check resolve chain values in debugging/tracing)
  • Load test to confirm allocation reduction

Summary by cubic

Reduced context copying in the resolver pipeline by using Object.create to derive lightweight contexts. This cuts allocations in deep resolve chains and should lower CPU and memory overhead.

  • Refactors
    • withResolveChain and withResolveChainOfType now return derived contexts; only resolveChain is new.
    • invokeResolverWithProps sets monitoring and resolverId on a derived context instead of spreading.
    • resolvePropsWithHints uses a derived context when overriding resolveId and building the chain.

Written for commit 4d7bc23. Summary will update on new commits.

Summary by CodeRabbit

  • Performance
    • Optimized resolver context handling for improved efficiency and more accurate metrics attribution

withResolveChain, withResolveChainOfType, and invokeResolverWithProps were spreading the entire context (10+ fields) just to change 1-2 fields. Use Object.create for prototypal inheritance so only changed properties are allocated. These functions are called recursively in the resolve pipeline, so this saves O(depth * fields) allocations per resolve chain.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions
Copy link
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 1.164.1 update
  • 🎉 for Minor 1.165.0 update
  • 🚀 for Major 2.0.0 update

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Introduces two new helper functions (withResolveChain and withResolveChainOfType) that create lightweight derived contexts using prototypal inheritance instead of spread-based shallow copies. Internal functions (resolvePropsWithHints and invokeResolverWithProps) are updated to utilize these new helpers for improved memory efficiency.

Changes

Cohort / File(s) Summary
Context Derivation Refactoring
engine/core/resolver.ts
Added withResolveChain and withResolveChainOfType helper functions to create prototype-based derived contexts. Updated resolvePropsWithHints and invokeResolverWithProps to use these helpers instead of spread-based context copies, improving memory efficiency while maintaining functional behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Through prototype chains we softly hop,
No spreading fields—inheritance's top!
Derived contexts light as morning dew,
Where resolver chains find paths anew.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: replacing context spreading with Object.create to reduce copying in the resolver pipeline, which matches the core objective of the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/reduce-resolver-context-copying

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

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

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

🧹 Nitpick comments (1)
engine/core/resolver.ts (1)

379-383: Avoid double context derivation on the resolveId path.

At Line [381] + Line [387], this branch creates one derived context for resolveId, then another in withResolveChain. You can allocate once and set resolveId on that single derived object.

♻️ Suggested simplification
-            let chainCtx = ctx;
-            if (resolveId) {
-              chainCtx = Object.create(ctx) as typeof ctx;
-              chainCtx.resolveId = resolveId;
-            }
+            const chainCtx = withResolveChain(ctx, { type: "prop", value: _key });
+            if (resolveId) {
+              chainCtx.resolveId = resolveId;
+            }
             const resolved = await resolvePropsWithHints(
               props[key],
               hint as HintNode<T[typeof key]>,
-              withResolveChain(chainCtx, { type: "prop", value: _key }),
+              chainCtx,
               opts,
             );

Also applies to: 387-387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/core/resolver.ts` around lines 379 - 383, The code currently creates
two derived contexts when resolveId is set (once by creating chainCtx from ctx
and then again inside withResolveChain); change this to allocate a single
derived context: if resolveId is present, create one Object.create(ctx)
instance, set chainCtx.resolveId = resolveId, and pass that same chainCtx into
withResolveChain so no second derivation occurs; update usages of chainCtx, ctx,
resolveId, and the withResolveChain call accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@engine/core/resolver.ts`:
- Around line 264-266: The created contexts use Object.create(ctx) (e.g., where
derived.resolveChain is set) which makes resolvables and resolvers inherited and
thus lost when middleware spreads ctx with { ...ctx, next }; change those
Object.create(ctx) usages (the derived context creations around resolveChain and
similar spots) to create a shallow own-property clone instead (e.g., const
derived = { ...ctx } as TContext) and then set derived.resolveChain =
ctx.resolveChain.concat(resolverType); repeat this change for the other two
occurrences so resolvables and resolvers become own properties and survive the
middleware spread.

---

Nitpick comments:
In `@engine/core/resolver.ts`:
- Around line 379-383: The code currently creates two derived contexts when
resolveId is set (once by creating chainCtx from ctx and then again inside
withResolveChain); change this to allocate a single derived context: if
resolveId is present, create one Object.create(ctx) instance, set
chainCtx.resolveId = resolveId, and pass that same chainCtx into
withResolveChain so no second derivation occurs; update usages of chainCtx, ctx,
resolveId, and the withResolveChain call accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e02918 and 4d7bc23.

📒 Files selected for processing (1)
  • engine/core/resolver.ts

Comment on lines +264 to +266
const derived = Object.create(ctx) as TContext;
derived.resolveChain = ctx.resolveChain.concat(resolverType);
return derived;
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

🧩 Analysis chain

🏁 Script executed:

cat -n engine/core/resolver.ts | head -300 | tail -50

Repository: deco-cx/deco

Length of output: 1750


🏁 Script executed:

cat -n engine/core/resolver.ts | sed -n '250,290p'

Repository: deco-cx/deco

Length of output: 1425


🏁 Script executed:

cat -n engine/core/resolver.ts | sed -n '450,470p'

Repository: deco-cx/deco

Length of output: 766


🏁 Script executed:

# Search for patterns that enumerate own keys on ctx/context objects
rg -nP --type=ts -C2 \
'Object\.(keys|values|entries)\(\s*(ctx|context)\s*\)|for\s*\(\s*const\s+\w+\s+in\s+(ctx|context)\s*\)|\{\s*\.\.\.\s*(ctx|context)\s*\}|JSON\.stringify\(\s*(ctx|context)\s*\)' engine/

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Search for resolver function implementations
rg -nP --type=ts 'export\s+(const|function)\s+\w*[Rr]esolver|const\s+\w*[Rr]esolver\s*=' engine/core/ | head -20

Repository: deco-cx/deco

Length of output: 539


🏁 Script executed:

# Search for spread operator on context-like variables more broadly
rg -nP --type=ts '\{\s*\.\.\.' engine/ | grep -i 'ctx\|context' | head -20

Repository: deco-cx/deco

Length of output: 612


🏁 Script executed:

# Search for Object.keys, Object.values, Object.entries usage
rg -nP --type=ts 'Object\.(keys|values|entries)' engine/ | head -20

Repository: deco-cx/deco

Length of output: 1853


🏁 Script executed:

# Look for JSON.stringify usage on context
rg -nP --type=ts 'JSON\.stringify' engine/ | grep -i 'ctx\|context'

Repository: deco-cx/deco

Length of output: 167


🏁 Script executed:

# Check if there are any actual breaking usages by searching all resolver implementations
rg -nP --type=ts 'for\s*\(\s*\w+\s+in\s+' engine/core/resolver.ts

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

cat -n engine/middleware.ts | sed -n '25,45p'

Repository: deco-cx/deco

Length of output: 551


🏁 Script executed:

# Get more context around the middleware resolver call
cat -n engine/middleware.ts | sed -n '1,60p'

Repository: deco-cx/deco

Length of output: 1374


🏁 Script executed:

# Check what calls middleware and if it uses the Object.create'd contexts
rg -nP --type=ts 'withResolveChain|withResolveChainOfType' engine/

Repository: deco-cx/deco

Length of output: 492


🏁 Script executed:

# Check if the context spreading in tests would actually catch this issue
cat -n engine/core/mod.test.ts | sed -n '100,120p'

Repository: deco-cx/deco

Length of output: 779


🏁 Script executed:

# Check what properties are set as own vs inherited in context creation
rg -nP --type=ts 'resolveChain|monitoring|resolverId|resolvers|resolvables' engine/core/resolver.ts | head -30

Repository: deco-cx/deco

Length of output: 1354


🏁 Script executed:

# Search for middleware implementations in the codebase
fd -e ts -e tsx | xargs grep -l 'ResolverMiddleware' | head -10

Repository: deco-cx/deco

Length of output: 159


🏁 Script executed:

# Check if any middleware actually accesses context properties
rg -nP --type=ts 'ctx\.(resolvers|resolvables|monitoring|resolverId)' engine/ | head -20

Repository: deco-cx/deco

Length of output: 683


🏁 Script executed:

# Trace the flow: where are Object.create'd contexts used in resolver chain
rg -nP --type=ts 'withResolveChain|compose' engine/core/resolver.ts | head -20

Repository: deco-cx/deco

Length of output: 372


🏁 Script executed:

# Check if tests would catch this issue with middleware
rg -nP --type=ts 'compose.*middleware' engine/

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Check what happens to inherited properties when spread
cat -n engine/middleware.ts | sed -n '20,40p'

Repository: deco-cx/deco

Length of output: 790


Compatibility break: middleware spread loses inherited context properties

Using Object.create(ctx) at lines 264, 275, and 459 makes resolvables and resolvers inherited rather than own properties. The middleware composer at line 34 spreads context with { ...ctx, next }, which copies only own properties, losing the inherited resolvables and resolvers fields required for resolver execution. Any middleware implementation or resolver flow through the middleware layer will break when these inherited properties are accessed.

Problematic code location
engine/middleware.ts:34
return resolver(p, { ...ctx, next });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/core/resolver.ts` around lines 264 - 266, The created contexts use
Object.create(ctx) (e.g., where derived.resolveChain is set) which makes
resolvables and resolvers inherited and thus lost when middleware spreads ctx
with { ...ctx, next }; change those Object.create(ctx) usages (the derived
context creations around resolveChain and similar spots) to create a shallow
own-property clone instead (e.g., const derived = { ...ctx } as TContext) and
then set derived.resolveChain = ctx.resolveChain.concat(resolverType); repeat
this change for the other two occurrences so resolvables and resolvers become
own properties and survive the middleware spread.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant