From 61e67ef89c01d8e1eccea6643a86ad59b9e590d3 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Mon, 2 Nov 2026 21:24:52 +0200 Subject: [PATCH 1/3] perf(lazy-zod): keep what a stand-in is built from in fields, not closures 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. --- .../lazy-zod/src/__tests__/internal-z.spec.ts | 33 ++++++++++ libs/lazy-zod/src/internal-z.ts | 66 ++++++++++++++----- libs/lazy-zod/src/lazy-z.ts | 11 ++-- 3 files changed, 90 insertions(+), 20 deletions(-) diff --git a/libs/lazy-zod/src/__tests__/internal-z.spec.ts b/libs/lazy-zod/src/__tests__/internal-z.spec.ts index e4d061e01..0d5be74b9 100644 --- a/libs/lazy-zod/src/__tests__/internal-z.spec.ts +++ b/libs/lazy-zod/src/__tests__/internal-z.spec.ts @@ -472,4 +472,37 @@ describe('internalZ', () => { const typed: internalZ.ZodType = schema; expect(typed.parse(value)).toEqual({ a: 'x' }); }); + + it('makes no function per stand-in: two alike stand-ins hold the same functions', () => { + const functionsOf = (schema: unknown) => + Object.values(targetOf(schema)).filter((value) => typeof value === 'function'); + const builds: Array<() => unknown> = [ + () => internalZ.string(), + () => internalZ.object({ a: internalZ.number() }), + () => internalZ.string().min(1).optional(), + () => internalZ.union([internalZ.string(), internalZ.number()]).optional(), + ]; + + for (const build of builds) { + const first = functionsOf(build()); + expect(first.length).toBeGreaterThan(0); + expect(functionsOf(build())).toEqual(first); + } + }); + + it('lets a built stand-in go of the arguments it was made with', () => { + const shape = { name: internalZ.string() }; + const options = [internalZ.literal('a'), internalZ.literal('b')]; + const object = internalZ.object(shape); + const union = internalZ.union(options).optional(); + + object.parse({ name: 'x' }); + union.parse('a'); + + for (const target of [object, union].map(targetOf)) { + const held = Object.values(target).flatMap((value) => (Array.isArray(value) ? value : [value])); + expect(held).not.toContain(shape); + expect(held).not.toContain(options); + } + }); }); diff --git a/libs/lazy-zod/src/internal-z.ts b/libs/lazy-zod/src/internal-z.ts index fad0779ba..bedc9e257 100644 --- a/libs/lazy-zod/src/internal-z.ts +++ b/libs/lazy-zod/src/internal-z.ts @@ -2,37 +2,71 @@ import { z as realZ, type ZodTypeAny } from 'zod'; import { HEAVY_FACTORIES, LEAF_FACTORIES } from './factories'; import { LAZY_TARGET, LazyZodSchema, wrapLazy } from './lazy-schema'; -import { createLazyZ, type StandInFactory, type z } from './lazy-z'; +import { createLazyZ, type SchemaFactory, type StandInFactory, type z } from './lazy-z'; import { withoutRecursionWalk } from './memoizer'; import { isLazy } from './utils'; const BUILT_WITH_PARENT: unique symbol = Symbol('builtWithParent'); -class InternalLazySchema extends LazyZodSchema { +const NO_ARGS: readonly unknown[] = Object.freeze([]); + +/** A stand-in keeps what it is built from in fields: a closure per stand-in would cost more than the stand-in. */ +abstract class InternalLazySchema extends LazyZodSchema { readonly [BUILT_WITH_PARENT]: boolean; + private args: readonly unknown[]; - constructor(factory: () => ZodTypeAny, builtWithParent: boolean) { - super(factory); + protected constructor(args: readonly unknown[], builtWithParent: boolean) { + super(InternalLazySchema.buildOnce); + this.args = args; this[BUILT_WITH_PARENT] = builtWithParent; } + private static buildOnce(this: InternalLazySchema): ZodTypeAny { + const args = this.args; + this.args = NO_ARGS; + return this.buildWith(args); + } + + protected abstract buildWith(args: readonly unknown[]): ZodTypeAny; + protected override build(factory: () => ZodTypeAny): ZodTypeAny { - return withoutRecursionWalk(factory); + return withoutRecursionWalk(() => factory.call(this)); } override chain(method: string, args: unknown[]): unknown { - return internalStandIn( - () => { - const real = this.materialize() as unknown as Record ZodTypeAny>; - return real[method](...args.map(withBuiltChildren)); - }, - this[BUILT_WITH_PARENT] && args.every(holdsOnlyLeaves), - ); + return wrapLazy(new ChainedStandIn(this, method, args, this[BUILT_WITH_PARENT] && args.every(holdsOnlyLeaves))); + } +} + +class FactoryStandIn extends InternalLazySchema { + constructor( + private readonly factory: SchemaFactory, + private readonly namespace: object, + args: readonly unknown[], + builtWithParent: boolean, + ) { + super(args, builtWithParent); + } + + protected buildWith(args: readonly unknown[]): ZodTypeAny { + return this.factory.apply(this.namespace, args.map(withBuiltChildren)); } } -function internalStandIn(build: () => ZodTypeAny, builtWithParent: boolean): unknown { - return wrapLazy(new InternalLazySchema(build, builtWithParent)); +class ChainedStandIn extends InternalLazySchema { + constructor( + private readonly parent: InternalLazySchema, + private readonly method: string, + args: readonly unknown[], + builtWithParent: boolean, + ) { + super(args, builtWithParent); + } + + protected buildWith(args: readonly unknown[]): ZodTypeAny { + const real = this.parent.materialize() as unknown as Record; + return real[this.method](...args.map(withBuiltChildren)); + } } function targetOf(value: unknown): LazyZodSchema | undefined { @@ -85,8 +119,8 @@ function withBuiltChildren(arg: unknown): unknown { return changed ? Object.create(Object.getPrototypeOf(arg), descriptors) : arg; } -const deferEveryFactory: StandInFactory = (name, args, build) => - internalStandIn(() => build(args.map(withBuiltChildren)), LEAF_FACTORIES.has(name) && args.every(holdsOnlyLeaves)); +const deferEveryFactory: StandInFactory = (name, args, factory, namespace) => + wrapLazy(new FactoryStandIn(factory, namespace, args, LEAF_FACTORIES.has(name) && args.every(holdsOnlyLeaves))); /** FrontMCP's own `z`: every factory returns a stand-in that builds its leaf children with it. Not public API. */ export const internalZ: typeof realZ = createLazyZ( diff --git a/libs/lazy-zod/src/lazy-z.ts b/libs/lazy-zod/src/lazy-z.ts index 9268737bd..7abc7c317 100644 --- a/libs/lazy-zod/src/lazy-z.ts +++ b/libs/lazy-zod/src/lazy-z.ts @@ -19,9 +19,12 @@ import { LazyZodSchema, wrapLazy } from './lazy-schema'; * target owns nothing, so no invariant applies; every trap forwards to * `namespace` instead. Exported for tests — the public entry point is `z`. */ -export type StandInFactory = (name: string, args: unknown[], build: (args: unknown[]) => ZodTypeAny) => unknown; +export type SchemaFactory = (...args: unknown[]) => ZodTypeAny; -const compoundStandIn: StandInFactory = (_name, args, build) => wrapLazy(new LazyZodSchema(() => build(args))); +export type StandInFactory = (name: string, args: unknown[], factory: SchemaFactory, namespace: object) => unknown; + +const compoundStandIn: StandInFactory = (_name, args, factory, namespace) => + wrapLazy(new LazyZodSchema(() => factory.apply(namespace, args))); export function createLazyZ( namespace: typeof realZ, @@ -31,8 +34,8 @@ export function createLazyZ( return new Proxy({} as typeof realZ, { get(_target, key, receiver) { if (typeof key === 'string' && factories.has(key)) { - const realFactory = (namespace as unknown as Record ZodTypeAny>)[key]; - return (...args: unknown[]) => standIn(key, args, (built) => realFactory.apply(namespace, built)); + const realFactory = (namespace as unknown as Record)[key]; + return (...args: unknown[]) => standIn(key, args, realFactory, namespace); } return Reflect.get(namespace, key, receiver); }, From f43ad8c9258fc33c252d4edc1bfabfd06ec38b8c Mon Sep 17 00:00:00 2001 From: David Antoon Date: Mon, 2 Nov 2026 22:52:58 +0200 Subject: [PATCH 2/3] fix(lazy-zod): drop a stand-in's arguments and parent only once it has 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. --- .../lazy-zod/src/__tests__/internal-z.spec.ts | 20 +++++++++++++++++++ libs/lazy-zod/src/internal-z.ts | 15 +++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/libs/lazy-zod/src/__tests__/internal-z.spec.ts b/libs/lazy-zod/src/__tests__/internal-z.spec.ts index 0d5be74b9..76930a1b7 100644 --- a/libs/lazy-zod/src/__tests__/internal-z.spec.ts +++ b/libs/lazy-zod/src/__tests__/internal-z.spec.ts @@ -505,4 +505,24 @@ describe('internalZ', () => { expect(held).not.toContain(options); } }); + + it('lets a built chained stand-in go of the stand-in it was chained on', () => { + const parent = internalZ.object({ a: internalZ.string() }); + const extended = parent.extend({ b: internalZ.number() }); + + extended.parse({ a: 'x', b: 1 }); + + expect(Object.values(targetOf(extended))).not.toContain(targetOf(parent)); + }); + + it('builds from the same arguments when a build that threw is tried again', () => { + const shape = { a: internalZ.string() }; + const conflicting = { message: 'm', error: 'e' }; + const object = internalZ.object(shape, conflicting); + const optional = internalZ.object(shape, conflicting).optional(); + + for (const schema of [object, optional, object, optional]) { + expect(() => schema.parse({ a: 1 })).toThrow('Cannot specify both `message` and `error` params'); + } + }); }); diff --git a/libs/lazy-zod/src/internal-z.ts b/libs/lazy-zod/src/internal-z.ts index bedc9e257..0ad04ff86 100644 --- a/libs/lazy-zod/src/internal-z.ts +++ b/libs/lazy-zod/src/internal-z.ts @@ -22,9 +22,9 @@ abstract class InternalLazySchema extends LazyZodSchema { } private static buildOnce(this: InternalLazySchema): ZodTypeAny { - const args = this.args; + const built = this.buildWith(this.args); this.args = NO_ARGS; - return this.buildWith(args); + return built; } protected abstract buildWith(args: readonly unknown[]): ZodTypeAny; @@ -54,18 +54,23 @@ class FactoryStandIn extends InternalLazySchema { } class ChainedStandIn extends InternalLazySchema { + private parent: InternalLazySchema | undefined; + constructor( - private readonly parent: InternalLazySchema, + parent: InternalLazySchema, private readonly method: string, args: readonly unknown[], builtWithParent: boolean, ) { super(args, builtWithParent); + this.parent = parent; } protected buildWith(args: readonly unknown[]): ZodTypeAny { - const real = this.parent.materialize() as unknown as Record; - return real[this.method](...args.map(withBuiltChildren)); + const real = (this.parent as InternalLazySchema).materialize() as unknown as Record; + const built = real[this.method](...args.map(withBuiltChildren)); + this.parent = undefined; + return built; } } From 05732fe849c46c543531bc3c5e1c054f5720ac8d Mon Sep 17 00:00:00 2001 From: David Antoon Date: Tue, 3 Nov 2026 09:43:00 +0200 Subject: [PATCH 3/3] test(e2e): raise two browser budgets by this branch's measured growth over #1016 On main 998ca63ab 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. --- .../demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts b/apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts index a0604c357..e4bc6c730 100644 --- a/apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts +++ b/apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts @@ -25,10 +25,10 @@ import { const BUDGETS: Array<{ entry: string; label: string; maxBytes: number }> = [ { entry: 'create-tool.ts', label: 'create() + one tool', maxBytes: 1_340_056 }, - { entry: 'create-webmcp-tool.ts', label: 'create() + WebMcpPlugin + one tool', maxBytes: 1_346_921 }, + { entry: 'create-webmcp-tool.ts', label: 'create() + WebMcpPlugin + one tool', maxBytes: 1_347_378 }, ]; -const VITE_INITIAL_CHUNK_MAX_BYTES = 964_390; +const VITE_INITIAL_CHUNK_MAX_BYTES = 964_827; // Packages create() never needs: the JavaScript parser behind @enclave-vm/ast (#885) and the MCP client (#886) const NEVER_BUNDLED: Record = {