Skip to content

perf(lazy-zod): keep less per stand-in, so startup retains less heap - #1021

Merged
frontegg-david merged 4 commits into
perf/p04-lazy-sdk-subsystemsfrom
perf/p05-startup-heap
Nov 3, 2026
Merged

frontegg-david merged 4 commits into
perf/p04-lazy-sdk-subsystemsfrom
perf/p05-startup-heap

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1016

Refs #948

What and why

Follow-up to the 1.10.9 performance work (#914, #919 to #925, #932, #934 to #943). #948 asks that a FrontMCP server, after startup and the first tools/call, have about as much new space committed as the official MCP SDK's server (16.8 MB, against FrontMCP's 33.6 MB), with RSS dropping accordingly and no cold-start or throughput regression, by cutting about 10 MB of live heap at startup.

This PR measures where that young generation comes from, cuts what FrontMCP's own code wasted the most, and lists what is left, with numbers. The acceptance is not met: the new space is still 32 MiB. Reaching 16 MiB needs about 6 MB less young-allocated data alive at the first tools/call (17.5 → about 11.5 MB). FrontMCP's own startup objects that could go are worth well under that. Most of the rest comes from dependencies and from the size of the SDK's single CommonJS file (below).

Why V8 doubles the young generation

Reproduced on this base (the benchmark's FrontMCP server, 1a4d98a36): v8.getHeapSpaceStatistics() reports new_space 33,554,432 bytes (32 MiB, two 16 MiB semi-spaces) at the first tools/call answer, and 16,777,216 (16 MiB) for the official SDK server, in every run.

V8 starts with 1 MiB semi-spaces and doubles them when the bytes that survived young collections (copied or promoted, mark-compacts included) since the last doubling exceed the semi-space size. --trace-gc-nvp of the base server through the first answer (MB = 10^6 bytes; "since growing" is the running total the rule compares, reset at each doubling):

GC ms Allocated Promoted Copied in new space Since growing Semi-space after
scavenge 44 8.93 2.17 0.66 2.84 2 MiB
scavenge 62 2.34 0.82 0.50 1.32 2 MiB
scavenge 76 2.46 0.72 0.80 2.84 4 MiB
scavenge 108 4.41 0.98 1.54 2.51 4 MiB
scavenge 125 3.64 1.84 1.12 5.47 8 MiB
scavenge 161 9.27 1.56 2.91 4.47 8 MiB
scavenge 205 6.56 2.89 1.65 9.00 16 MiB
scavenge 324 18.83 1.42 3.95 5.38 16 MiB
mark-compact 372 4.44 5.06 0 10.43 16 MiB

The 8 MiB doubling comes as require('@frontmcp/sdk') reaches @frontmcp/auth. The 16 MiB doubling comes while the SDK loads @frontmcp/protocol, the MCP SDK's server and ajv: 9.00 MB had survived since the 8 MiB doubling, against a limit of 8.39 MB (8 MiB). The official server ends at 8 MiB, with 6.63 MB survived since its last doubling. For FrontMCP to stay at 8 MiB, everything that survives from the 8 MiB doubling to the first answer (19.4 MB here) would have to stay under 8.39 MB. A long-lived object is counted up to twice (copied, then promoted; 30.6 MB survive in all for 17.5 MB alive at the end), so that means about 6 MB less young-allocated data alive at the first answer.

Where the live young heap comes from

Measured directly: with 64 MiB semi-spaces (--min-semi-space-size=64 --max-semi-space-size=64) no scavenge runs before the first answer. The probe then forces one scavenge, and its survivors are exactly the young-allocated objects still alive. The number is the same in every run:

Young-allocated data alive at the first tools/call answer MB
Official MCP SDK server 9.15
FrontMCP, base 17.54
FrontMCP, this PR 17.07 (−0.47)

The same method, with a scavenge forced at the end of each startup phase, gives what each phase allocated and still holds when it ends (base → this PR, MB): require('@frontmcp/sdk') 13.39 → 12.80, the @Tool/@App/@FrontMcp decorators 0.87 → 0.87, scope setup up to listening 2.25 → 2.21, listening to the first answer 1.68 → 1.71. Loading the SDK and what it requires is most of it.

An allocation-tracked heap snapshot of the base at the first answer attributes those objects to the code that allocated them. Only the kinds V8 allocates young are counted, so the split is approximate:

Origin MB
Module source text V8 keeps for the compiled code 6.1
… @frontmcp/sdk's CommonJS file (index.js, one-byte) 2.17
… zod: 116 files, 62 of them two-byte (a character above U+00FF, such as — in a comment, makes V8 store the whole file at two bytes per character) 2.13
… @frontmcp/auth, utils, protocol and FrontMCP's other packages ~0.7
… the MCP SDK, ajv, Express and their dependencies ~1.1
zod schema objects and zod's own module setup: the MCP SDK's types.js builds its schemas when it loads (1.06), and 1,087 of FrontMCP's lazy schemas are built before the first answer (about 1.2) 3.5
The Streamable HTTP transport: @hono/node-server and Node's http2, tls and bundled undici, loaded after listening 1.46
Express 5, body-parser and mime-db ~1.2
lazy-zod stand-ins (3,248 at the first answer) 0.94
FrontMCP's module setup: classes, esbuild's export getters, flows, registries ~1.5

The 1,087 lazy schemas built at startup are FrontMCP's own metadata and option schemas, which their parse walks in full: @FrontMcp metadata 446, @App 136, @Tool 116, the @Flow decorators about 150 (http:request 91, tools:call-tool 20), auth options 61, the session of the first request 60, the config plugin and service 62. zod 4's object parse normalizes every key of a shape, .optional() reads its inner schema's optin, and defaults have to run, so a parse builds the whole tree whatever keys the input has.

What this PR changes

lazy-zod: a stand-in keeps what it is built from in fields, not in closures. internalZ, which every FrontMCP schema in sdk, auth and utils uses since #1010/#1011, made two closures and two contexts for each stand-in: the factory wrapper in createLazyZ and the build closure in deferEveryFactory (or the chained-method closure in chain). Together they cost more than the stand-in object and its Proxy. A stand-in is now a FactoryStandIn (zod's factory, its namespace and the arguments) or a ChainedStandIn (the parent stand-in, the method and the arguments). All of them hand their base class the same static builder. Once it has built, a stand-in drops its arguments, and a chained stand-in its parent, as the closure was dropped before. A build that throws keeps them, so trying again builds the same schema. The public z's compound stand-ins make one closure instead of two. Nothing else changes: the same factories build the same schemas from the same arguments, in the same order (a chained stand-in builds its parent first, then its arguments' children). createLazyZ and StandInFactory aren't exported from the package entry.

Effect: −0.47 MB of young-allocated data alive at the first answer, and −0.42 MB of heap after a full GC over HTTP (−0.43 MB over stdio), in every round. In a microbenchmark (6 runs each), making 2,000 metadata-shaped object schemas of 20 stand-ins each takes 24.6 to 27.7 ms of CPU instead of 29.9 to 37.0 ms. Building and parsing them takes the same time either way (302 to 336 ms against 302 to 344 ms).

The tables under Results measure this PR's first commit. The second commit keeps a stand-in's arguments when its build throws, and drops a chained stand-in's parent once it has built. Dropping the parent takes a further 6 KB off the young data alive at the first answer (17.069 → 17.063 MB, three runs each) and 0.04 MB off the live heap after a full GC over HTTP (31.04 → 31.00 MB, 8 rounds, all lower). CPU time to the first answer over HTTP doesn't change: +3.1 ms per round (−19.3 to +19.5), against −0.7 ms (−15.6 to +11.1) between two identical copies of the first commit (14 rounds of 3, load 24 to 38).

Tried and dropped (measured, not shipped)

  • Building the @FrontMcp metadata schemas only for its one parse and dropping them afterwards: 17.08 → 17.56 MB (+0.48). The scope builds the same option schemas again for its own parses.
  • Mangling the SDK file's local names (esbuild with --minify-identifiers --minify-syntax --keep-names on the published file): the source shrinks from 2.17 to 1.57 MB, but live young data goes up to 18.50 MB. --keep-names adds a __name() call for every class and function.
  • Stand-ins holding a bound function instead of fields: −0.30 MB, against −0.47 MB for fields.

What is left, with evidence

What remains is mostly outside the code this PR touches. Even all of FrontMCP's own startup objects together come to less than the 6 MB needed:

  1. zod's two-byte sources (upstream). In a scratch copy of the benchmark, escaping the characters above U+007F in zod's v4/core, v4/classic, v4/mini and v3, the MCP SDK's CommonJS build and qs (what scripts/escape-non-ascii.mjs already does to FrontMCP's own output) brings live young data from 17.54 to 16.68 MB (−0.86). The new space stays at 32 MiB. 44 of the 62 two-byte zod files are locales, which zod loads on every import (1.10: startup and first-request costs that come from dependencies #951, item 4).
  2. The MCP SDK v1 server. server/index.js loads zod v3 (v3/types.cjs, 282 KB two-byte), zod/v4-mini, zod-to-json-schema and ajv, and types.js builds about 1.06 MB of zod schemas when it loads. The official server, on SDK v2, doesn't load these. FrontMCP's Server comes from server/index.js, so the server can't run without them.
  3. Express 5, body-parser and mime-db: about 1.2 MB, of which mime-db's table is 0.40 MB. The official server mounts its handler on Node's http.
  4. The SDK's single CommonJS file: 2.17 MB of source text for every subsystem, used or not. perf(sdk): set up jobs, workflows, channels, remote MCP, the ESM loader and OAuth flows on first use #1016 sets up unused subsystems on first use, but their text still loads, since esbuild can't split CommonJS output.
  5. Startup schema parsing: about 1.2 MB for the 1,087 schemas listed above, which zod builds in full to validate and fill in defaults.

Results

Apple M4 Pro (14 cores, 48 GiB), macOS 15.6.1, Node.js 24.22.1, on AC power, with other jobs running on the machine (1-minute load average given per run; it was 22 to 175 during this work). The server is the benchmark's FrontMCP implementation (frontmcp build --target node), run with node, against npm packed builds of 1a4d98a36 (this PR's base: main at 91531eb plus #1016) and of this branch, each installed into a scratch copy of the implementation and built with npm run build. The two dist/node/bench.bundle.js are byte-identical, and only @frontmcp/lazy-zod differs between the package sets. The official SDK server is the benchmark's official-sdk implementation. Runs use the benchmark's harness functions (startServer, HttpSession, connectStdio, coldStartOnce, throughputOnce, rssOfTree) in randomized, interleaved rounds. "Per round" is the median, over rounds, of this branch's value minus the base's in the same round, with its range. MB is 10^6 bytes.

Memory, run A: 16 rounds, one start per build and transport (1-minute load average 101 to 124). Live heap: 12 rounds with --expose-gc, a full GC right after the first answer (load 63 to 100).

Base This PR Per round Rounds lower
new_space committed at the first answer, HTTP and stdio 33.55 MB 33.55 MB 0 all equal
Live heap after a full GC at the first answer, HTTP 31.45 MB 31.04 MB −0.42 (−0.43 to −0.41) 12/12
Live heap after a full GC at the first answer, stdio 25.57 MB 25.14 MB −0.43 (−0.51 to −0.42) 12/12
Heap used at the first answer, HTTP (no forced GC) 35.47 MB 35.05 MB −0.42 (−1.99 to −0.20) 16/16
RSS at the first answer, HTTP 135.54 MB 135.17 MB +0.02 (−4.52 to +3.47) 7/16
RSS after two tools/calls and 1 s, HTTP 135.23 MB 135.23 MB +0.25 (−4.55 to +4.42) 6/16
RSS after two tools/calls and 1 s, stdio 118.42 MB 118.84 MB +0.61 (−4.75 to +3.47) 4/16

RSS doesn't move measurably. The 0.4 MB of heap is below RSS's round-to-round spread, and the young generation, which RSS follows, is the same size. "Heap used" without a GC isn't a live-heap measure over stdio: with no mark-compact before the first stdio answer, it swings by 11 MB depending on whether a scavenge lands just before the answer.

Cold start and CPU, run B: 12 rounds of 3 starts per build and scenario (load 90 to 134, median 107). Run C: 16 rounds of 3 (load 74 to 142, median 123).

Base This PR Per round, B Per round, C
Cold start, HTTP, p50 / p90 (B) 358.5 / 465.9 ms 369.9 / 507.1 ms +20.7 (−125.8 to +98.4), 4/12 lower +24.8 (−680.8 to +273.8), 7/16
Cold start, stdio, p50 / p90 (B) 279.5 / 465.2 ms 252.4 / 430.8 ms −4.7 (−291.0 to +190.2), 7/12 −13.0 (−223.3 to +390.3), 10/16
CPU time to the first answer, HTTP (B) 358.1 ms 353.1 ms +0.7 (−98.8 to +47.1), 5/12 +0.1 (−125.3 to +26.8), 8/16
CPU time to the first answer, stdio (B) 243.1 ms 247.5 ms +4.8 (−41.7 to +56.9), 5/12 +5.8 (−35.0 to +52.4), 6/16
  • Cold start swings by hundreds of milliseconds from round to round at this load, and the medians move in opposite directions over HTTP and stdio, so no change shows. CPU time over HTTP is unchanged.
  • CPU time to the first stdio answer is a few milliseconds higher in the per-round medians. That comes from where one scavenge lands. In a third run (20 rounds of 5, load 125 to 175, per round +14.9 ms), starts with a scavenge just before the first answer used about 20 ms more CPU than starts without one (base 283.0 against 259.8 ms, this PR 277.9 against 265.8 ms). The scavenge happened before the answer in 67 of 100 starts of this PR and 15 of 100 of the base. Comparing starts without it, the per-round median is +2.5 ms. Measured later, after 200 more tools/calls and 1 s (12 rounds, load 104 to 124), this PR used −7.8 ms of CPU per round (−27.4 to +12.6, 8/12 lower): the work moves earlier, and none is added.

Throughput: the benchmark's throughputOnce, concurrency 1, 16 and 64, 2 s warm-up and 5 s measured per level, 10 rounds (load 22 to 111, median 49). No errors.

Base This PR Per round
Requests/s, c=1 1,135 1,583 +12.6% (−36.0 to +331.6)
Requests/s, c=16 4,045 4,835 +20.7% (−9.8 to +48.9)
Requests/s, c=64 4,091 4,178 +9.7% (−44.6 to +142.5)
RSS after load 462.7 MB 453.5 MB −1.9% (−16.3 to +9.7)

A first run of 8 rounds (load 45 to 64) had −0.0%, −1.1% and −1.9% per round, with ranges of −71% to +672%. The change isn't on the per-call path: a stand-in's parse is bound to its built schema on first use, as before. So these differences are load, not the change, and neither run shows a regression.

User-facing change

  • What changed: nothing a user can see. @frontmcp/lazy-zod's public z, lazyZ, LazyZodSchema, internalZ and the schemas they build behave as before. A FrontMCP server at the first tools/call holds about 0.42 MB less heap.
  • Before → after: browser bundles (demo-e2e-bundle-size, base build swapped in for the before column): create() + one tool 1,322,622 → 1,323,079 bytes minified (+457; gzipped 346,320 → 346,489), with WebMcpPlugin 1,329,811 → 1,330,268 (+457), Vite's initial chunk 943,585 → 944,020 (+435). All within the budgets of that base, so no budget changes then.
  • After merging perf(sdk): set up jobs, workflows, channels, remote MCP, the ESM loader and OAuth flows on first use #1016 with main at 998ca63 (measured against perf(sdk): set up jobs, workflows, channels, remote MCP, the ESM loader and OAuth flows on first use #1016's head dae7555): create() + one tool 1,339,520 → 1,339,977 (+457), still within 1,340,056; with WebMcpPlugin 1,346,697 → 1,347,154 (+457), so its budget rises by exactly that, 1,346,921 → 1,347,378; Vite's initial chunk 964,258 → 964,695 (+437), so its budget rises by exactly that, 964,390 → 964,827. nx run sdk:test passes 9,257 tests in 660 suites, nx run lazy-zod:test 369, and bundle-size 23.
  • frontmcp.dev pages affected (if known): none.
  • Skills catalog entries updated: none. No entry describes lazy-zod's internals or startup memory.

Tests

  • New unit tests in libs/lazy-zod/src/__tests__/internal-z.spec.ts, written first: two alike internalZ stand-ins (a leaf, an object, a chained leaf, a chained union) hold the same functions, so no stand-in makes one of its own. This failed on the base, where each held its own closure. A built stand-in no longer holds the shape or options array it was made with. This one passed on the base too, where the closure that held them was dropped, and now guards the new fields. Two more pass on the base and failed on the first commit: a built chained stand-in no longer holds the stand-in it was chained on, and a stand-in whose build threw builds from the same arguments when tried again. On the first commit, the arguments were dropped before building, so the second parse of an internalZ.object whose build had thrown built z.object() and accepted any object. The existing parity suites (every factory and chain against Zod's own schemas, same issues and JSON Schema, leaf children built with their parent, compound children deferred, memoizer handling, introspection) pass unchanged.
  • On the final commit: nx test lazy-zod passes 369 (internal-z.ts and lazy-z.ts 100% covered), utils 2,025, auth 2,811, di 245 (1 todo), guard 271, and nx run sdk:test (--maxWorkers=50%) 8,487 tests in 611 suites. nx run lazy-zod:lint reports 0 errors and no new warning (the 12 warnings are the existing any defaults in the z and internalZ type namespaces), and tsc --noEmit on libs/lazy-zod/tsconfig.lib.json and tsconfig.spec.json reports no error.
  • e2e: demo-e2e-bundle-size 23 (sizes above), demo-e2e-dts-consumer 22, demo-e2e-stdio-transport 30, demo-e2e-standalone 53. Standalone needs nx run observability:build first: without libs/observability/dist, its three observability tests fail with "@frontmcp/observability is not installed". node --test scripts/*.test.mjs passes 84, among them the Worker export-condition and browser build guards.
  • The benchmark server answered every tools/call correctly (the harness checks each result) in about 750 starts of each build over HTTP and stdio.
  • node scripts/fix-unused-imports.mjs changes nothing, and Prettier passes on every changed file.

Summary by CodeRabbit

  • Bug Fixes
    • Schema creation now behaves more consistently when schemas are built lazily. Repeated attempts to build a schema that fails validation continue to produce the same error.
    • Successfully built schemas release temporary construction state, helping avoid retaining unnecessary references.
    • Equivalent stand-ins now share function references, improving consistency when working with alike schemas.

…sures

internalZ made two closures and their contexts per stand-in, which cost more
than the stand-in itself. A stand-in now keeps zod's factory, its namespace and
the arguments (or the parent and method it was chained from) and drops the
arguments once built. The public z's stand-ins make one closure instead of two.
…s built

A stand-in cleared its arguments before building, so a build that threw left
it empty: the next parse built the schema without them (an internalZ.object
whose build threw became z.object() and accepted any object). It now drops
them after a successful build, as the closure was dropped before.

A chained stand-in also kept its parent stand-in, and the parent's built
schema, for as long as it lived. It now drops the parent once built.
@frontegg-david
frontegg-david marked this pull request as ready for review November 2, 2026 20:57
@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The lazy-zod stand-in implementation now stores factory inputs and materializes schemas through dedicated factory-backed and chained classes. Successful builds clear retained arguments and parent references. The factory contract passes the factory and namespace directly. Tests cover shared function references, released references, and repeated validation errors. Browser bundle budgets were increased.

Changes: Lazy Zod stand-ins

Layer / File(s) Summary
Factory contract and dispatch
libs/lazy-zod/src/lazy-z.ts, libs/lazy-zod/src/internal-z.ts
The stand-in factory contract now accepts a schema factory and namespace. Factory-backed stand-ins invoke the factory with that namespace as its receiver.
Materialization and reference cleanup
libs/lazy-zod/src/internal-z.ts, libs/lazy-zod/src/__tests__/internal-z.spec.ts, apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts
Stand-ins use shared build logic and clear stored arguments or parent references after successful builds. Tests cover shared function references, released references, and repeated validation errors. Browser bundle budgets increased.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related issues

  • agentfront/frontmcp#948 — This PR reduces data retained by lazy-zod stand-ins. The issue also targets startup live heap, but its stated new-space target remains unmet.

Possibly related PRs

  • agentfront/frontmcp#1011 — It introduced internalZ, deferred stand-ins, and the factory contract that this PR changes.

Suggested reviewers: alexmercerpo

Merge Risk: Minimal · up to 05732

No actionable merge-blocking risk is identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: reducing per-stand-in memory retention in lazy-zod during startup.
Description check Passed The description includes complete What and why, User-facing change, and Tests sections. It explains the implementation, measured results, bundle impact, validation, and known limitations.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 4 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between f43ad8c and 05732fe.

Files selected for processing: 1
  • apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 5 included reviews per hour; 0 remain after this review.

@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-11-03T08:01:05.591Z
Commit: b4c22d02

@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

… over #1016

On main 998ca63 with #1016, the lazy stand-ins add 457 bytes to create() + WebMcpPlugin + one tool (1,346,697 -> 1,347,154) and 437 to Vite's initial chunk (964,258 -> 964,695). Those budgets rise by exactly that: 1,346,921 -> 1,347,378 and 964,390 -> 964,827. create() + one tool (1,339,977) still fits 1,340,056.
@frontegg-david
frontegg-david merged commit 0ad3fa9 into main Nov 3, 2026
20 checks passed
@frontegg-david
frontegg-david deleted the perf/p05-startup-heap branch November 3, 2026 07:54
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.

2 participants