Skip to content

perf: validate options with a precompiled schema - #5746

Merged
alexander-akait merged 8 commits into
mainfrom
perf/precompile-options-schema
Sep 20, 2026
Merged

alexander-akait merged 8 commits into
mainfrom
perf/precompile-options-schema

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 20, 2026

Copy link
Copy Markdown
Member

Summary

schema-utils's validate() compiles the options schema with ajv on its first call, which cost 117ms of every new Server() — measured over eight interleaved runs at 117.3 ± 2.8ms, against 0.07ms for each call after it. It is a one-time price every user pays on every start.

lib/options.check.js is that schema precompiled by ajv's standalone codegen, generated by scripts/generate-schema-check.mjs and verified current by lint:schema-check. The constructor asks it first and only falls back to schema-utils when it rejects, so the happy path never loads ajv and an invalid config still gets the same message — the same approach webpack uses for its own schema. The constructor drops to 1.0ms and time-to-listening for a hello-world falls from 565 ± 14ms to 523 ± 25ms.

The generator refuses to emit if the schema grows a construct it would mistranslate (a minLength other than 1, an unsupported instanceof), so a future schema change fails generation rather than silently changing semantics.

What kind of change does this PR introduce?

perf

Did you add tests for your changes?

Yes, test/schema-check.test.js. The existing validate-options suite already catches a validator that wrongly accepts invalid options; it cannot see one that wrongly rejects valid options, because Server then falls back to schema-utils, which accepts them — every test passes while the startup cost silently returns. Stubbing an always-false validator fails 17 of the new tests and zero of the existing 163. The generated validator was also checked to agree with schema-utils on all 183 cases of the validate-options corpus plus 62 hand-written ones, with no mismatches, and the 35 error-message snapshots are unchanged.

Does this PR introduce a breaking change?

No. Validation accepts and rejects exactly what it did before, with identical error messages.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a — no public behaviour or option changes. lib/options.check.js is generated; npm run fix:schema-check regenerates it and npm run lint:schema-check (part of lint) fails if it is stale.

Use of AI

AI-assisted (Claude Code). It was used to profile startup, to write the generator and tests, and to run the differential and A/B measurements. Every number quoted here came from a run, not an estimate: the arms were interleaved to absorb machine drift, and two other candidate optimisations were measured and discarded rather than shipped (parallelising the lazy import()s showed no consistent win because module loading is compile-bound, and only ~14ms of listen() is ours). All output was reviewed before committing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA


Generated by Claude Code

Summary by CodeRabbit

  • Performance

    • Improved development server startup by precompiling option validation, reducing startup time by approximately 115 ms.
  • Bug Fixes

    • Preserved clear, readable validation error messages when configuration options are invalid.
    • Improved validation for multi-compiler configurations by using the appropriate compiler settings.
  • Tests

    • Added coverage for valid and invalid server configuration options, including multi-compiler setups, to help ensure validation remains accurate across supported configurations.

`schema-utils`'s `validate()` compiles the options schema with ajv on its
first call. That cost 117ms of every `new Server()` — measured over eight
interleaved runs, 117.3 +/- 2.8ms, against 0.07ms for each call after it.
It is a one-time price every user pays on every start.

`lib/options.check.js` is that schema precompiled by `ajv`'s standalone
codegen. The constructor asks it first and only falls back to
`schema-utils` when it rejects, so the happy path never loads ajv and an
invalid config still gets the same message. webpack solves its own schema
the same way, and the generated validator agrees with `schema-utils` on
all 183 cases of the `validate-options` corpus plus 62 hand-written ones.

The constructor drops to 1.0ms, and time-to-listening for a hello-world
falls from 565 +/- 14ms to 523 +/- 25ms.
@changeset-bot

changeset-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2bc55dd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 69280711-aab5-44c1-a862-73320663f76c

📥 Commits

Reviewing files that changed from the base of the PR and between bbd85ef and 6357fea.

📒 Files selected for processing (1)
  • types/lib/Server.d.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.


Walkthrough

The change adds an Ajv-based generator for a standalone options validator. Server selects the dev-server compiler and uses the precompiled validator before falling back to schema-utils. New scripts maintain the generated file, and tests cover valid, invalid, and multi-compiler validation cases. Type declarations and lint-format ignores cover the generated module. A changeset records the patch release.

Priority: ➖ Normal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: using a precompiled schema to validate options for improved performance.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 6 files.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

`compiler.hooks.validate` and `compiler.validate`'s lazy-schema and
precompiled-check parameters landed together in webpack 5.106, so the
hook doubles as the feature probe for them. When it is there, validation
goes through the compiler the way webpack's own plugins do, which also
lets `validate: false` turn it off.

The direct path stays as the fallback, with a TODO to drop it once the
minimum supported webpack carries the hook. It is also what runs when no
compiler was passed, which is how the server is constructed for `apply()`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e7f01b6c-f838-4d7d-bd65-776611ff907f

📥 Commits

Reviewing files that changed from the base of the PR and between ceea14f and 547aeda.

📒 Files selected for processing (1)
  • lib/Server.js

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

Comment thread lib/Server.js Outdated
`compiler.validate` honours that compiler's own `validate` option, so
asking the first child of a `MultiCompiler` read a policy that was never
about these options: with `validate: false` on child 0 and the dev server
configured on child 1, invalid options reached normalization instead of
throwing.

`getCompilerOptions` already had the selection rule — the child naming
`devServer`, else the one targeting the web, else the first. It moves to
`findDevServerCompiler`, which both callers now use so the two cannot
drift apart again.

Reported by CodeRabbit on #5746.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 3f7915a4-7eb6-4da2-b59f-5b5f8fd4fb6f

📥 Commits

Reviewing files that changed from the base of the PR and between 547aeda and bbd85ef.

📒 Files selected for processing (2)
  • lib/Server.js
  • test/schema-check.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/schema-check.test.js

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.

Comment thread lib/Server.js Outdated
@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.65432% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.74%. Comparing base (e44bdae) to head (2bc55dd).

Files with missing lines Patch % Lines
lib/Server.js 87.65% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5746      +/-   ##
==========================================
- Coverage   90.83%   90.74%   -0.09%     
==========================================
  Files          13       14       +1     
  Lines        6282     6329      +47     
==========================================
+ Hits         5706     5743      +37     
- Misses        576      586      +10     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov reported 6 uncovered lines in the diff, all of them fallbacks:
`findDevServerCompiler` returning the first child when none names
`devServer` or targets the web, and the direct `schema-utils` call taken
when there is no compiler to ask, which is how the server is constructed
for `apply()`.

lib/Server.js's changed lines go from 70/76 to 76/76 covered.
Each `webpack()` call leaves a compiler holding a file-system cache and
its purge timer. Node's test runner runs files concurrently, so those
stayed alive alongside the browser suites for the rest of the run.

Windows Node 22 timed out in `test/e2e/overlay.test.js` on the previous
commit, which was test-only; that suite already uses most of its 400s
there, and this is the load these tests were adding to it.
Used as a plugin the server is constructed without a compiler, so
validating in the constructor had no `validate` policy to read and threw
before `apply()` could bring the compiler the options are about. Validate
in the constructor only when one is passed, and in `apply()` otherwise.
@alexander-akait
alexander-akait merged commit 3e73e45 into main Sep 20, 2026
15 of 17 checks passed
@alexander-akait
alexander-akait deleted the perf/precompile-options-schema branch September 20, 2026 19:53
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