Skip to content

Conversation

@jacopobonomi
Copy link

This PR improves schema initialization performance by (~5-15%) by deferring the creation of the "~standard" property until it's actually accessed.

Problem

The ~standard property implements the https://github.com/standard-schema/standard-schema but is rarely used in typical applications. Currently, this object is created eagerly for every schema during initialization, adding unnecessary overhead:

  inst["~standard"] = {
    validate: (value: unknown) => { ... },
    vendor: "zod",
    version: 1 as const,
  };

Solution

Convert the property to a lazy getter that only creates the object when accessed, then caches the result:

  Object.defineProperty(inst, "~standard", {
    get() {
      const standardSchema = {
        validate: (value: unknown) => { ... },
        vendor: "zod",
        version: 1 as const,
      };
      // Cache the result after first access
      Object.defineProperty(this, "~standard", {
        value: standardSchema,
        writable: false,
        enumerable: false,
        configurable: false,
      });
      return standardSchema;
    },
    enumerable: false,
    configurable: true,
  });

@colinhacks
Copy link
Owner

Great idea. I appreciate you looking into the initialization perf. How are you benchmarking that? I'd like to have a decent benchmark for initialization if you have one - feel free to add it under /packages/bench.

General approach is good. This same pattern is already implemented as util.cached in the utils.ts file. Try using that function instead.

@colinhacks colinhacks requested a review from Copilot October 21, 2025 17:26
Repository owner deleted a comment from coderabbitai bot Oct 21, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR optimizes schema initialization performance by converting the ~standard property from eager initialization to lazy loading with caching. The Standard Schema protocol support object is now only created when accessed, reducing unnecessary overhead for applications that don't use this feature.

Key Changes:

  • Converted ~standard property to a lazy getter using Object.defineProperty
  • Added caching mechanism to store the result after first access
  • Reformatted the async fallback in the validate function for better readability

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 6, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The $ZodType class's "~standard" property initialization was refactored to use lazy initialization via util.defineLazy. Previously, the property was initialized directly and eagerly. Now, the object creation is deferred until the property is first accessed. The change maintains the same validate function, vendor, and version metadata. The public interface and external behavior remain unchanged; only the internal initialization approach was modified from eager to deferred execution.

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf: lazy initialize ~standard schema property' directly and clearly summarizes the main change—converting eager initialization to lazy initialization for a specific schema property.
Description check ✅ Passed The description is well-related to the changeset, explaining both the problem (eager initialization overhead), the solution (lazy initialization with caching), and includes concrete code examples demonstrating the approach.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 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 8e4739f and 6a7cdb1.

📒 Files selected for processing (1)
  • packages/zod/src/v4/core/schemas.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{js,jsx,ts,tsx,mjs,cjs,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce line width of 120 characters via Biome formatting

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{js,jsx,ts,tsx,mjs,cjs}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ES5-style trailing commas in JavaScript/TypeScript code

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Allow the any type in TypeScript (noExplicitAny off)
Allow non-null assertions in TypeScript (noNonNullAssertion off)
Write TypeScript to pass strict mode with exactOptionalPropertyTypes enabled
Use NodeNext module resolution semantics for imports in TypeScript
Target ES2020 language features in TypeScript source

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (CLAUDE.md)

Allow parameter reassignment for performance-sensitive code (noParameterAssign off)

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{js,mjs,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)

**/*.{js,mjs,ts,tsx}: Use .js extensions in import specifiers (e.g., import { z } from "./index.js")
Don’t use require(); use ESM import statements

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{js,mjs,cjs,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

Do not leave log statements (e.g., console.log, debugger) in tests or production code

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/src/v4/core/{schemas.ts,core.ts}

📄 CodeRabbit inference engine (.cursor/rules/zod-internals.mdc)

Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/src/v4/core/schemas.ts

📄 CodeRabbit inference engine (.cursor/rules/zod-internals.mdc)

packages/zod/src/v4/core/schemas.ts: Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout
Define computed internal properties using util.defineLazy() to avoid circular dependencies
Implement schema parse functions following the standard structure: type check, push invalid_type issue on mismatch, optionally coerce/transform, and return payload
For $ZodDiscriminatedUnion, compute and merge propValues lazily from options; ensure each option provides the discriminator key and that values are unique
Ensure readonly wrapper types (e.g., $ZodReadonly) pass through values for discriminator support in unions

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/src/v4/core/{schemas.ts,checks.ts}

📄 CodeRabbit inference engine (.cursor/rules/zod-internals.mdc)

When adding issues, push well-formed payload.issues entries including code, expected (when applicable), input, inst, and optional path/message/continue

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)

Write source code in TypeScript (TypeScript-first codebase)

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/**

📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)

Make core Zod library changes in the main package at packages/zod/

Files:

  • packages/zod/src/v4/core/schemas.ts
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Define computed internal properties using util.defineLazy() to avoid circular dependencies
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Follow the documented steps when creating new schemas: define schema/ internals interfaces, constructor via $constructor(), init with $ZodType.init(), implement parse(), add lazy properties, and tests
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,core.ts} : Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/util.ts : Use util.cached() for expensive computed properties and normalization caching
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-project-guide.mdc:0-0
Timestamp: 2025-10-21T17:28:01.210Z
Learning: Applies to packages/zod/** : Make core Zod library changes in the main package at packages/zod/
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Define computed internal properties using util.defineLazy() to avoid circular dependencies

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,core.ts} : Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : For $ZodDiscriminatedUnion, compute and merge propValues lazily from options; ensure each option provides the discriminator key and that values are unique

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Ensure readonly wrapper types (e.g., $ZodReadonly) pass through values for discriminator support in unions

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Import Zod in tests as `import * as z from "zod/v4"`

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Implement schema parse functions following the standard structure: type check, push invalid_type issue on mismatch, optionally coerce/transform, and return payload

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Use import type for type-only imports in tests (e.g., `import type { ... }`)

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:28:01.210Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-project-guide.mdc:0-0
Timestamp: 2025-10-21T17:28:01.210Z
Learning: Applies to packages/zod/** : Make core Zod library changes in the main package at packages/zod/

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/util.ts : Use util.cached() for expensive computed properties and normalization caching

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Adhere to best practices: verify wrapper passthrough, prefer util.defineLazy for computed props, follow existing schema patterns, use precise TypeScript types, and document complex internals

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:24:39.708Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-21T17:24:39.708Z
Learning: Applies to packages/zod/package.json : Provide the zod/source export condition pointing to TypeScript source for development

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:24:39.708Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-21T17:24:39.708Z
Learning: Applies to packages/zod/package.json : Expose multiple entry points and versions (v4 default from v4/classic/external.js, v4/core, v4/mini, v3, mini) via conditional exports

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:24:39.708Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-21T17:24:39.708Z
Learning: Applies to packages/zod/src/v4/classic/external.js : Ensure the default v4 export resolves from src/v4/classic/external.js

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
🔇 Additional comments (1)
packages/zod/src/v4/core/schemas.ts (1)

304-316: Nice optimization with lazy initialization!

This change looks solid. Using util.defineLazy here is the right call for defining a lazy property on the instance. The property gets created on first access and cached automatically, which should give you that performance boost without changing the external behavior.

About the race condition concern from the previous review: that's actually not an issue here. The getter function executes synchronously to create the ~standard object, and JavaScript's single-threaded event loop ensures this happens atomically. The async stuff in the validate function is separate from the initialization itself.

One thing to note: the ~standard property is part of the public API (not under _zod), but the lazy initialization pattern still applies well here since it's effectively treating it like a computed property that gets cached.

Based on learnings.


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.

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