Skip to content

perf: validate options with a precompiled schema - #2413

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

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

Conversation

@alexander-akait

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

Copy link
Copy Markdown
Member

Summary

Validating the options compiles the schema with ajv on the first call, which cost 159ms of the first middleware() against 0.08ms for each call after it. Every webpack-dev-server start pays it once.

src/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. internalValidate asks it first and only falls through to compiler.validate/schema-utils when it rejects, so the happy path never loads ajv, options.json stays unread until something is actually wrong, and an invalid config still gets the same message — the same approach webpack uses for its own schema. Validation drops to 1.9ms.

Pairs with webpack/webpack-dev-server#5746, which does the same for its own schema. The two are worth reading together: fixing only the dev server moves ~76ms into this package, because it then pays the ajv load the dev server's constructor used to. On its own that is −7.3% of a hello-world's time-to-listening; with this one it is −22.4% (565 ± 14ms → 438 ± 17ms, eight interleaved runs per arm).

Checking here rather than passing the validator to compiler.validate as its fourth argument is deliberate: that parameter exists in webpack 5.110, but the peer range is ^5.101.0 and I could not confirm it across all of it. Checking first is version-agnostic and behaviourally identical, including for compiler.options.validate === false.

What kind of change does this PR introduce?

perf

Did you add tests for your changes?

Yes, test/schema-check.test.js. The existing validation-options suite already catches a validator that wrongly accepts invalid options; it cannot see one that wrongly rejects valid options, because validation then falls back to the real schema, which accepts them — every test passes while the startup cost silently returns. The generated validator was also checked to agree with schema-utils on all 76 cases of the validation-options corpus, 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. src/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, with arms interleaved to absorb machine drift. The full suite was run before and after: the 74 failures in test/logging.test.js are pre-existing and environmental (jest needs Node ≥24.9 to require ESM execa), confirmed identical on a clean checkout. 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 startup performance by using precompiled options validation, reducing startup time by approximately 155 ms in typical usage.
  • Validation

    • Middleware options are checked for unsupported properties, invalid types, and out-of-range values.
    • Invalid configurations are rejected consistently with clear validation errors.
  • Compatibility

    • Preserved validation behavior across supported compiler configurations.
  • Release

    • Included in a patch release of webpack-dev-middleware.

Validating the options compiles the schema with ajv on the first call,
which cost 159ms of the first `middleware()` against 0.08ms for each call
after it. Every webpack-dev-server start pays it once.

`src/options.check.js` is that schema precompiled by `ajv`'s standalone
codegen. `internalValidate` asks it first and only falls through to
`compiler.validate`/`schema-utils` when it rejects, so the happy path
never loads ajv, `options.json` stays unread until something is actually
wrong, and an invalid config still gets the same message. The generated
validator agrees with `schema-utils` on all 76 cases of the
`validation-options` corpus.

Checking here rather than passing the validator to `compiler.validate` as
its fourth argument keeps this working across the whole supported webpack
range, not only versions carrying that parameter.

Validation drops to 1.9ms, and a dev server's time-to-listening for a
hello-world falls from 523 +/- 25ms to 438 +/- 17ms.
@changeset-bot

changeset-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 31d61d7

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-middleware 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: c132bed9-68ef-4bef-adac-86630648f2b9

📥 Commits

Reviewing files that changed from the base of the PR and between 583e6d0 and 31d61d7.

📒 Files selected for processing (1)
  • test/schema-check.test.js

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


Walkthrough

The PR adds an Ajv generator and a generated options validator. internalValidate uses the generated validator before the existing schema-validation path. Tests cover accepted and rejected option shapes and fallback behavior. Type declarations describe the generated module. Package scripts, lint configuration, formatting rules, and a patch changeset support the generated file.

Priority: ⬇️ Low

🚥 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 main change: validating options with a precompiled schema for performance.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 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, the
precompiled check is handed to the compiler the way webpack's own plugins
hand it theirs, rather than being run before it.

The direct path stays as the fallback, keeping the existing TODO to drop
it once the minimum supported webpack carries the hook.
@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.29%. Comparing base (233389a) to head (31d61d7).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2413      +/-   ##
==========================================
+ Coverage   97.16%   97.29%   +0.12%     
==========================================
  Files          12       13       +1     
  Lines        1660     1664       +4     
==========================================
+ Hits         1613     1619       +6     
+ Misses         47       45       -2     

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

bjohansebas
bjohansebas previously approved these changes Sep 20, 2026
codecov flagged the fallback as the uncovered part of the diff: CI
installs webpack 5.110, where `compiler.hooks.validate` always exists, so
nothing reached the path taken on webpack < 5.106.

Replacing the frozen `compiler.hooks` with a copy that lacks `validate`
exercises it, and the spy asserts the fallback validated rather than the
compiler. Without these two tests, lines 232, 233, 236 and 238 are
uncovered; with them, every statement in `internalValidate` is.
@alexander-akait
alexander-akait merged commit 6113936 into main Sep 20, 2026
22 checks passed
@alexander-akait
alexander-akait deleted the perf/precompile-options-schema branch September 20, 2026 18:16
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