Problem/Motivation

The Patternkit validation system validates pattern data against JSON schemas both before and after field processor execution. Each validation call currently triggers a full schema compilation via the Swaggest library's Schema::import(), which parses and compiles the JSON schema from scratch.

When a page has multiple Patternkit blocks—or the same pattern used multiple times—the same schema is compiled repeatedly. With no reuse, 20 blocks using 5 unique patterns result in 20 schema compilations instead of 5. Profiling showed that schema validation accounts for roughly 64% of uncached request time (~160 ms of ~249 ms in a measured scenario), making redundant compilation a major cost for first-time visitors, after cache invalidation, and during development.

Interaction with #3563760 (render-time validation): #3563760: Add Pattern Content Validation During Rendering adds validation during pattern render in Pattern::preRenderPatternElement()—after config is set but before processSchemaValues()—so that invalid content is caught before rendering and can display a graceful error instead of failing. That change introduces an additional validation (and thus schema compilation) for every pattern render. Without caching, the cost of that new validation is paid once per block: 20 blocks mean 20 compilations of the same 5 schemas. This issue is what keeps that cost acceptable: with caching, the same schema string compiles once per request and is reused for every block that uses that pattern. So #3563760: Add Pattern Content Validation During Rendering increases how often we need a compiled schema at render time; this issue ensures we do not recompile the same schema repeatedly. The two together deliver both safe render-time validation and good performance.

Steps to reproduce

  1. Enable Patternkit and create a content type or layout that allows multiple Patternkit blocks.
  2. Add many blocks that use the same pattern (e.g. 20 blocks using 5 distinct patterns).
  3. Clear page cache and load the page (uncached request).
  4. Profile the request (e.g. with Xdebug or Drupal's profiling tools): observe that a large share of time is spent in schema parsing/compilation and that each block triggers a full schema compile with no reuse for identical schemas.

Proposed resolution

Add request-scoped caching of compiled schema instances in SchemaFactory so that identical schema strings reuse a single compiled Schema instance per request. This is intended to be applied together with (or after) #3563760: Add Pattern Content Validation During Rendering, so that the extra validation introduced at render time does not multiply schema compilation cost per block.

Implementation approach:

  • Use the Swaggest library's built-in schemasCache on Context (an \SplObjectStorage) so that repeated Schema::import() calls with the same decoded schema object return the cached instance.
  • Cache decoded JSON by schema string hash so the same schema string always yields the same object reference, which is required for Swaggest's cache lookup.
  • Lazy-initialize a static schemasCache and a static decoded-schema cache in SchemaFactory; both are request-scoped (no persistent storage).
  • Add a public static SchemaFactory::clearCache() for tests and explicit invalidation.

Expected impact (from profiling): for 20 blocks with 5 unique patterns, schema creations drop from 20 to 5 (~75% reduction), with a proportional reduction in schema overhead and a significant improvement in uncached request time (on the order of ~48% in the profiled scenario).

Scenarios that benefit most include:

  • First-time visitors
  • Pages loaded after cache invalidation
  • Development workflows with frequent cache clears
  • High-traffic sites experiencing cache churn

User interface changes

None. This is a performance-only change with no UI impact.

API changes

  • \Drupal\patternkit\Schema\SchemaFactory::createInstance()

    Behavior change: when called multiple times in the same request with the same schema string, the same SchemaContract instance is returned (object identity). Callers that rely on getting a new instance each time would be affected; typical usage does not.
  • New: \Drupal\patternkit\Schema\SchemaFactory::clearCache(): void

    Static method that clears the request-scoped schema and decoded-schema caches. Intended for tests and for explicit invalidation when needed.

Data model changes

None. No database or config schema changes.

Regression testing scenarios

The following is a list of relevant regression testing scenarios to keep in mind in the upcoming release.

Preconditions / Test Setup

  • Patternkit and required example/media modules are enabled.
  • Layout Builder flow is available (AJAX/off-canvas block config form).
  • Full-page editor flow is available (non-AJAX submit).
  • Test pattern includes a required string field, optional string field, required media field (where applicable), and nested/array required string field if schema supports it.

AJAX Submit Validation (Off-canvas / Layout Builder)

  • Valid submit succeeds when all required fields are populated.
  • Empty required string blocks submit and shows inline Value required.
  • Whitespace-only required string blocks submit and shows inline error.
  • After failed submit, correcting the field allows next submit to succeed.
  • Submit button is re-enabled after validation failure (not stuck disabled).
  • AJAX progress indicator is removed after validation failure.
  • Immediate submit without blur validates/submits latest typed value.
  • Rapid/double-click submit does not create stuck UI or duplicate behavior.

Non-AJAX Submit Validation (Full-page Form)

  • Valid submit succeeds.
  • Empty required string blocks submit and shows inline error.
  • Whitespace-only required string blocks submit and shows inline error.

Media Field Validation

  • Required media field left empty blocks submit.
  • Inline validation error appears on media field/editor control.
  • Error clears after valid media selection and submit succeeds.
  • Required image attribute scenario (for example alt) shows field-level error if empty.

Schema Structure Coverage

  • Nested required string empty is blocked with field-level error.
  • Array-item required string empty is blocked with field-level error.
  • Multiple required fields empty show all relevant errors.

Compatibility / Stability

  • Pattern with no required fields still submits normally.
  • Re-opening form after AJAX DOM replacement does not produce duplicate hook behavior.
  • Repeated open/edit/submit cycles in the same session remain stable.

Known Limitation

  • Client-side validation enforces non-empty required strings; server-side parity for this specific rule is tracked separately.

Release notes snippet

  • Performance: Request-scoped caching in SchemaFactory so that identical JSON schemas are compiled once per request and reused. Pages with many Patternkit blocks (or the same pattern repeated) see large reductions in schema validation time on uncached requests. Especially beneficial when render-time validation (e.g. #3563760: Add Pattern Content Validation During Rendering) is in use, since each block render uses the same cached schema instead of recompiling.

Issue fork patternkit-3572072

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

slucero created an issue. See original summary.

slucero’s picture

Status: Active » Needs review
slucero’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

This has been validated internally and approved for merging. I've added noteworthy regression testing scenarios to the issue summary and am marking this ready for moving forward.

  • slucero committed 56d0f794 on 9.1.x
    [#3572072] Add request-scoped schema caching in SchemaFactory.
    
slucero’s picture

Status: Reviewed & tested by the community » Fixed

Merged for inclusion in the 9.1.3 release.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.