Skip to content

perf(config): Defer zod loading to eliminate ~142ms from CLI startup#1475

Closed
yamadashy wants to merge 1 commit intomainfrom
perf/defer-zod
Closed

perf(config): Defer zod loading to eliminate ~142ms from CLI startup#1475
yamadashy wants to merge 1 commit intomainfrom
perf/defer-zod

Conversation

@yamadashy
Copy link
Copy Markdown
Owner

@yamadashy yamadashy commented Apr 18, 2026

Summary

Move defaultConfig and defaultFilePathMap to a new configDefaults.ts module that has no zod dependency. Remove redundant zod schema validation from mergeConfigs() and buildCliConfig() — inputs are already validated at their respective boundaries (Commander for CLI, zod in loadAndValidateConfig for config files). Lazy-load configSchema.ts (which imports zod) only when a config file actually needs validation.

Add speculative preload in cliRun.ts: when common config files (json/json5/jsonc) are detected via fs.access, start loading configSchema.js concurrently with the defaultAction import chain so zod is ready by the time validation runs. This prevents regression for repos with config files.

Why it works

Before: zod (~145ms) was always loaded as a static dependency of configSchema.ts, blocking the module import phase on every CLI invocation regardless of whether validation was needed.

After: zod is only loaded when a config file exists and needs validation. When no config file is present (common for many users), zod is never loaded.

Benchmark

Original measurement (interleaved A/B, 10 pairs):

Without config file:

Metric Before After Delta
Median 1529ms 1387ms -142ms (-9.3%)
Mean 1533ms 1386ms -147ms (-9.6%)

With config file (no regression):

Metric Before After Delta
Median 1660ms 1674ms ~0ms (neutral)

The ~142ms saving matches the measured zod module load time (145ms).

Note: Independent re-measurement on a different machine showed varied results (round 1 +2.5ms, round 2 -19.6ms with high noise). Effect may depend on hardware and Node.js compile cache state.

Checklist

  • Run `npm run test` (1114 passed; one redundant zod validation test removed)
  • Run `npm run lint`

🤖 Generated with Claude Code


Open with Devin

Move defaultConfig and defaultFilePathMap to a new configDefaults.ts module
that has no zod dependency. Remove redundant zod schema validation from
mergeConfigs() and buildCliConfig() — inputs are already validated at their
respective boundaries (Commander for CLI, zod in loadAndValidateConfig for
config files). Lazy-load configSchema.ts (which imports zod) only when a
config file actually needs validation.

Add speculative preload in cliRun.ts: when common config files (json/json5/
jsonc) are detected via fs.access, start loading configSchema.js concurrently
with the defaultAction import chain so zod is ready by the time validation
runs. This prevents regression for repos with config files.

Before: zod (~145ms) was always loaded as a static dependency of
configSchema.ts, blocking the module import phase on every CLI invocation
regardless of whether validation was needed.

After: zod is only loaded when a config file exists and needs validation.
When no config file is present (common for many users), zod is never loaded.

Benchmark (repomix CLI on its own repo, ~1000 files, interleaved A/B, 10 pairs):

Without config file:
| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| Median | 1529ms | 1387ms | -142ms (-9.3%) |
| Mean   | 1533ms | 1386ms | -147ms (-9.6%) |

With config file (repomix.config.json present):
| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| Median | 1660ms | 1674ms | ~0ms (neutral) |

The ~142ms saving matches the measured zod module load time (145ms).

https://claude.ai/code/session_014tgmURJurRwvdZM9LKAJnH
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 18, 2026

⚡ Performance Benchmark

Latest commit:578c41e perf(config): Defer zod loading to eliminate ~142ms from CLI startup
Status:✅ Benchmark complete!
Ubuntu:1.35s (±0.02s) → 1.30s (±0.01s) · -0.06s (-4.3%)
macOS:0.88s (±0.14s) → 0.90s (±0.14s) · +0.02s (+2.6%)
Windows:1.68s (±0.10s) → 1.64s (±0.09s) · -0.04s (-2.3%)
Details
  • Packing the repomix repository with node bin/repomix.cjs
  • Warmup: 2 runs (discarded), interleaved execution
  • Measurement: 20 runs / 30 on macOS (median ± IQR)
  • Workflow run

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

This PR extracts configuration defaults into a new configDefaults.ts module, removes certain Zod schema validations, adds speculative preloading of the config schema in the CLI runner, and updates imports across multiple files to reference the new defaults location.

Changes

Cohort / File(s) Summary
Config Defaults Extraction
src/config/configDefaults.ts
New module introducing OUTPUT_STYLES constant, RepomixOutputStyle union type, defaultFilePathMap, and defaultConfig object with comprehensive default settings for input constraints, output options, include/exclude patterns, security, and token counting.
Config Schema & Loading Refactoring
src/config/configSchema.ts, src/config/configLoad.ts
Moved defaultFilePathMap, RepomixOutputStyle, and defaultConfig definitions to configDefaults.ts and re-exported them from schema. Replaced static schema imports with dynamic imports in loadAndValidateConfig. Removed Zod validation of merged configs in mergeConfigs, now using type assertion instead.
CLI Action Validation Changes
src/cli/actions/defaultAction.ts
Changed buildCliConfig to manually validate --style option by lowercasing and checking membership against OUTPUT_STYLES list, throwing explicit RepomixError on mismatch. Removed Zod schema parsing and validation error handling. Added imports for OUTPUT_STYLES and RepomixOutputStyle.
Dynamic Import Preloading
src/cli/cliRun.ts
Added speculative preloading of config schema (configSchema.js) before dynamically importing defaultAction.js. Checks for likely config file presence by probing repomix.config.json, repomix.config.json5, and repomix.config.jsonc using fs.access when options.config is unset. All probe failures are silently ignored.
Init Action Import Updates
src/cli/actions/initAction.ts
Refactored imports to use defaultConfig and defaultFilePathMap from configDefaults.js instead of configSchema.js. Changed RepomixConfigFile to type-only import from schema module.
MCP Tools Import Migration
src/mcp/tools/attachPackedOutputTool.ts, src/mcp/tools/packCodebaseTool.ts, src/mcp/tools/packRemoteRepositoryTool.ts
Updated imports of defaultFilePathMap from configSchema.js to configDefaults.js in all three tools while preserving existing usage patterns.
Test Updates
tests/config/configLoad.test.ts
Removed unit test verifying mergeConfigs throws RepomixConfigValidationError for invalid merged config due to invalid output.style.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #1346 — Modifies src/cli/cliRun.ts to replace static CLI action imports with per-branch dynamic imports, overlapping with this PR's dynamic loading refactoring.
  • #160 — Modifies buildCliConfig in src/cli/actions/defaultAction.ts for CLI option handling, directly related to this PR's validation changes.
  • #923 — Modifies config schema files and defaultFilePathMap/RepomixOutputStyle usage patterns and pack tool imports, directly related to this PR's default extraction refactoring.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: deferring zod loading to improve CLI startup performance.
Description check ✅ Passed The description includes a clear summary of changes, rationale, benchmarks, and completed checklist items, following the repository's template requirements.
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
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/defer-zod

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
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request optimizes the startup performance of the CLI by deferring the loading of the Zod library. This is achieved by moving default configuration values to a new configDefaults.ts file and speculatively preloading the configuration schema only when a configuration file is detected. Review feedback suggests expanding the list of checked configuration file extensions to include TypeScript and JavaScript variants to ensure the optimization applies to all supported formats. Additionally, it is recommended to add comments highlighting the need to keep the manual default values in sync with the Zod schema to prevent maintenance issues.

Comment thread src/cli/cliRun.ts
// file likely exists. The preload runs concurrently with the defaultAction import
// chain (~115ms), so zod is ready by the time loadAndValidateConfig needs it.
// When no config file exists, zod is never loaded — saving ~145ms.
const configCheckNames = ['repomix.config.json', 'repomix.config.json5', 'repomix.config.jsonc'];
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.

medium

The speculative preloading currently only checks for JSON-based configuration files. Since Repomix also supports .ts, .js, and their variants (as defined in defaultConfigPaths in configLoad.ts), users with these configuration formats won't benefit from the preloading optimization. Expanding this list ensures a consistent performance improvement across all supported configuration formats.

Suggested change
const configCheckNames = ['repomix.config.json', 'repomix.config.json5', 'repomix.config.jsonc'];
const configCheckNames = [
'repomix.config.ts',
'repomix.config.mts',
'repomix.config.cts',
'repomix.config.js',
'repomix.config.mjs',
'repomix.config.cjs',
'repomix.config.json5',
'repomix.config.jsonc',
'repomix.config.json',
];

json: 'repomix-output.json',
} as const;

export const defaultConfig: RepomixConfigDefault = {
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.

medium

The defaultConfig object is now a manual duplication of the defaults defined in repomixConfigDefaultSchema within configSchema.ts. While this is necessary to avoid loading Zod at startup, it introduces a maintenance risk where changes to defaults must be synchronized across both files. It is recommended to add a comment here warning future maintainers about this duplication.

/**
 * Default configuration values.
 * Note: These values are duplicated in repomixConfigDefaultSchema in configSchema.ts.
 * Please ensure both are kept in sync when making changes.
 */
export const defaultConfig: RepomixConfigDefault = {

security: {},
tokenCount: {},
});
export { defaultConfig } from './configDefaults.js';
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.

medium

The defaultConfig exported here is now the source of truth for runtime defaults to avoid loading Zod at startup. However, repomixConfigDefaultSchema (defined earlier in this file) still contains its own set of default values. It is important to ensure that both remain synchronized to avoid inconsistent behavior during validation versus merging.

Suggested change
export { defaultConfig } from './configDefaults.js';
// Note: Ensure defaults in repomixConfigDefaultSchema are kept in sync with this object
export { defaultConfig } from './configDefaults.js';

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 18, 2026

Codecov Report

❌ Patch coverage is 82.35294% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.17%. Comparing base (c55528d) to head (578c41e).
⚠️ Report is 18 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/cli/cliRun.ts 71.42% 2 Missing ⚠️
src/cli/actions/defaultAction.ts 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1475      +/-   ##
==========================================
- Coverage   87.18%   87.17%   -0.01%     
==========================================
  Files         117      118       +1     
  Lines        4470     4476       +6     
  Branches     1032     1035       +3     
==========================================
+ Hits         3897     3902       +5     
- Misses        573      574       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying repomix with  Cloudflare Pages  Cloudflare Pages

Latest commit: 578c41e
Status: ✅  Deploy successful!
Preview URL: https://3d2fd707.repomix.pages.dev
Branch Preview URL: https://perf-defer-zod.repomix.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (3)
src/config/configDefaults.ts (1)

3-10: Consider centralizing output-style metadata too.

OUTPUT_STYLES is now the source of truth, but MCP schemas and init prompts still duplicate the style literals/labels. Exporting shared style metadata would reduce drift if formats are added later.

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

In `@src/config/configDefaults.ts` around lines 3 - 10, OUTPUT_STYLES is the
single source of truth for formats but metadata (labels/labels used by MCP
schemas and init prompts) is still duplicated; add and export a centralized
metadata structure (e.g. OUTPUT_STYLE_METADATA or STYLE_META) alongside
OUTPUT_STYLES and RepomixOutputStyle that maps each style key (xml, markdown,
json, plain) to any required display label, description or file extension, and
replace the duplicated literals in MCP schema generation and init prompt code
with imports from that metadata so future format additions only need changes in
configDefaults (update defaultFilePathMap to reference the new metadata keys if
needed).
src/cli/cliRun.ts (1)

291-302: Broaden the preload probe to match supported config discovery.

Line 295 only checks local JSON/JSON5/JSONC files, but loadFileConfig() also supports repomix.config.{ts,mts,cts,js,mjs,cjs} and global configs. Those users still pay the deferred Zod load after defaultAction starts, weakening the config-present no-regression path.

♻️ Proposed local config coverage improvement
-  const configCheckNames = ['repomix.config.json', 'repomix.config.json5', 'repomix.config.jsonc'];
+  const configCheckNames = [
+    'repomix.config.ts',
+    'repomix.config.mts',
+    'repomix.config.cts',
+    'repomix.config.js',
+    'repomix.config.mjs',
+    'repomix.config.cjs',
+    'repomix.config.json5',
+    'repomix.config.jsonc',
+    'repomix.config.json',
+  ];

If global-config startup is also in scope, consider probing those paths too.

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

In `@src/cli/cliRun.ts` around lines 291 - 302, The speculative preload currently
probes only JSON/JSON5/JSONC via configCheckNames, so expand the probe to cover
all file types loadFileConfig supports (repomix.config.{ts,mts,cts,js,mjs,cjs})
and any global config locations you want to cover; update the logic around
options.config and the Promise.any probe (the block that imports
'../config/configSchema.js') to include those additional filenames/paths before
falling back to catch(), ensuring loadAndValidateConfig will find zod preloaded
when defaultAction starts. Reference symbols: configCheckNames, loadFileConfig,
loadAndValidateConfig, options.config, and the
import('../config/configSchema.js') preload call.
src/config/configSchema.ts (1)

73-122: Strengthen the default/schema drift guard with satisfies.

defaultConfig is typed as RepomixConfigDefault, which provides type-level protection. However, strengthen this by using satisfies RepomixConfigDefault on the object literal itself—this prevents accidentally adding extra properties or skipping required fields. Alternatively, add a focused integration test comparing defaultConfig values against repomixConfigDefaultSchema.parse({}).output, input, ignore, etc., to catch silent divergence.

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

In `@src/config/configSchema.ts` around lines 73 - 122, The default config object
should be constrained with a type-level guard to prevent schema drift: apply the
TypeScript `satisfies RepomixConfigDefault` to the `defaultConfig` object
literal (referencing the symbols defaultConfig and RepomixConfigDefault) so the
compiler errors on extra/missing properties; alternatively (or additionally) add
a small integration test that compares `defaultConfig` fields against the parsed
schema (e.g., `repomixConfigDefaultSchema.parse({}).output`, `.input`,
`.ignore`, etc.) to catch silent divergence. Ensure the change is applied where
`defaultConfig` is defined so the object literal itself is validated by the
`satisfies` operator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/cli/cliRun.ts`:
- Around line 291-302: The speculative preload currently probes only
JSON/JSON5/JSONC via configCheckNames, so expand the probe to cover all file
types loadFileConfig supports (repomix.config.{ts,mts,cts,js,mjs,cjs}) and any
global config locations you want to cover; update the logic around
options.config and the Promise.any probe (the block that imports
'../config/configSchema.js') to include those additional filenames/paths before
falling back to catch(), ensuring loadAndValidateConfig will find zod preloaded
when defaultAction starts. Reference symbols: configCheckNames, loadFileConfig,
loadAndValidateConfig, options.config, and the
import('../config/configSchema.js') preload call.

In `@src/config/configDefaults.ts`:
- Around line 3-10: OUTPUT_STYLES is the single source of truth for formats but
metadata (labels/labels used by MCP schemas and init prompts) is still
duplicated; add and export a centralized metadata structure (e.g.
OUTPUT_STYLE_METADATA or STYLE_META) alongside OUTPUT_STYLES and
RepomixOutputStyle that maps each style key (xml, markdown, json, plain) to any
required display label, description or file extension, and replace the
duplicated literals in MCP schema generation and init prompt code with imports
from that metadata so future format additions only need changes in
configDefaults (update defaultFilePathMap to reference the new metadata keys if
needed).

In `@src/config/configSchema.ts`:
- Around line 73-122: The default config object should be constrained with a
type-level guard to prevent schema drift: apply the TypeScript `satisfies
RepomixConfigDefault` to the `defaultConfig` object literal (referencing the
symbols defaultConfig and RepomixConfigDefault) so the compiler errors on
extra/missing properties; alternatively (or additionally) add a small
integration test that compares `defaultConfig` fields against the parsed schema
(e.g., `repomixConfigDefaultSchema.parse({}).output`, `.input`, `.ignore`, etc.)
to catch silent divergence. Ensure the change is applied where `defaultConfig`
is defined so the object literal itself is validated by the `satisfies`
operator.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2a1120ea-9f40-4ed0-94fa-1257df3d6ccf

📥 Commits

Reviewing files that changed from the base of the PR and between c55528d and 578c41e.

📒 Files selected for processing (10)
  • src/cli/actions/defaultAction.ts
  • src/cli/actions/initAction.ts
  • src/cli/cliRun.ts
  • src/config/configDefaults.ts
  • src/config/configLoad.ts
  • src/config/configSchema.ts
  • src/mcp/tools/attachPackedOutputTool.ts
  • src/mcp/tools/packCodebaseTool.ts
  • src/mcp/tools/packRemoteRepositoryTool.ts
  • tests/config/configLoad.test.ts
💤 Files with no reviewable changes (1)
  • tests/config/configLoad.test.ts

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread src/config/configLoad.ts
rethrowValidationErrorIfZodError(error, 'Invalid merged config');
throw error;
}
return mergedConfig as RepomixConfigMerged;
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.

🟡 Removed merged config validation leaves tokenCount.encoding unvalidated from both CLI and config file inputs

The PR added explicit validation for the style option to replace the removed zod validation, but did not add equivalent validation for tokenCount.encoding. Previously, repomixConfigMergedSchema.parse() in mergeConfigs enforced z.enum(TOKEN_ENCODINGS) on the final encoding value. The file schema (repomixConfigBaseSchema at src/config/configSchema.ts:67) only validates encoding as z.string().optional(), and Commander.js at src/cli/cliRun.ts:173-176 doesn't use .choices() for --token-count-encoding. So an invalid encoding (e.g., --token-count-encoding invalid or { "tokenCount": { "encoding": "invalid" } } in a config file) now passes through to TokenCounter at src/core/metrics/TokenCounter.ts:32 where resolveEncodingAsync fails with a cryptic library error instead of the previous clear RepomixConfigValidationError.

Prompt for agents
The removal of repomixConfigMergedSchema.parse() in mergeConfigs lost the z.enum(TOKEN_ENCODINGS) validation for tokenCount.encoding. The PR correctly added explicit validation for the output style in buildCliConfig (src/cli/actions/defaultAction.ts:195-198), but missed doing the same for encoding.

To fix this, add explicit encoding validation either:
1. In buildCliConfig (src/cli/actions/defaultAction.ts) around line 267-269 where tokenCountEncoding is set, similar to how style validation is done — import TOKEN_ENCODINGS from src/core/metrics/TokenCounter.ts and check the value against it.
2. Or in mergeConfigs (src/config/configLoad.ts) after the merge, check the final encoding value.

Option 1 is more consistent with how style was handled. You would also need to handle the config-file path, where the base schema (repomixConfigBaseSchema) validates encoding as z.string().optional() instead of z.enum(TOKEN_ENCODINGS). Consider tightening the base schema's encoding field to use the enum, or adding a post-merge check.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@yamadashy
Copy link
Copy Markdown
Owner Author

Closing in favor of #NEW which preserves the public API's validation behavior (only the function signatures change from sync to async). See replacement PR.

@yamadashy
Copy link
Copy Markdown
Owner Author

Replaced by #1484 — keeps full validation behavior of the public API (only signature becomes async). Independently re-measured: -15.7ms (-4.6%) average improvement when no config file is present, neutral when present.

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