Skip to content

perf(registry): cache linked Handlebars templates per component version - #1567

Open
ricardo-devis-agullo wants to merge 2 commits into
masterfrom
perf/registry-handlebars-template-cache
Open

perf(registry): cache linked Handlebars templates per component version#1567
ricardo-devis-agullo wants to merge 2 commits into
masterfrom
perf/registry-handlebars-template-cache

Conversation

@ricardo-devis-agullo

@ricardo-devis-agullo ricardo-devis-agullo commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

Every Handlebars render on the registry hot path re-runs utils.validator() + handlebars.template() (the template-link step). Upstream oc-template-handlebars@6.0.26 lib/render.js intends to cache the linked template per key but never hits:

const cached = options.key && cache.get(cacheType, options.key);
if (!cached) {
  ...
  linked = handlebars.template(options.template);
  cache.set(cacheType, options.key); // BUG: value argument omitted -> stores undefined
  ...
}

cache.get therefore misses forever (and linked is never assigned from the cached entry either, so a hit could not even be served). The registry artifact cache (file-contents in get-component.ts) only caches the precompiled spec (result of getCompiledTemplate), so the link step still runs per render inside ocTemplate.render (reached via oc-client TemplateRenderer -> client.renderTemplate).

Note: the referenced packages/oc/registry-performance-improvements.md does not exist in this worktree, so this implements item 1 from the task description against the code as found.

Fix (defensive, registry-side; node_modules untouched)

  • New packages/oc/src/registry/domain/linked-template-cache.ts: withLinkedTemplateCache(template) decorator. Misses delegate to the wrapped module (preserving its validation/error semantics exactly); after a successful render the spec is linked once and cached. Hits skip validator + link and run the cached linked fn against the current model. Fail-open throughout (link/load failures just delegate again).
  • Keying: options.key (component template hashKey on the registry path) in an LRU cache (reuses BoundedCache, default 500 entries). Keyless renders fall back to a WeakMap keyed by spec identity instead of a content hash (specs contain functions invisible to JSON, so hashing would be costly and collision-prone; identity is exact and GC-bounded).
  • Wiring in get-component.ts: the repository's handlebars module is wrapped once per registry and appended to the Client({ templates }) list after user templates (custom templates keep precedence; oc-client's own base copy loses via first-seen uniq), so the hot-path client.renderTemplate dispatches to the cached-link render.

Verify

  • New test/unit/registry-domain-linked-template-cache.js (10 tests): link-once-per-key, per-key independence, error propagation without caching, fail-open on link throw, linked-run throw without re-delegating, LRU eviction at bound, keyless identity caching, non-object passthrough, api preservation + wrap idempotency, default real-handlebars link path.
  • New test/unit/registry-routes-helpers-get-component-linked-cache.js (2 tests): two sequential rendered requests for one Handlebars component through the real oc-client + real template module return identical HTML while the upstream render (link step) runs exactly once.
  • Existing suites still pass: registry-routes-helpers-get-component*, registry-domain-register-templates, utils-bounded-cache (39 passing alongside new tests; 25 passing in final re-run).
  • tsc clean; biome check clean (incl. pre-commit hook).

Upstream note

Proper upstream fix is one line in oc-template-handlebars/lib/render.js: cache.set(cacheType, options.key, linked) (plus assigning linked = cached on hit). This PR deliberately does not touch node_modules; the registry-side cache makes renders immune to that bug regardless.

Benchmark note

bench:quick was attempted (baseline on master, comparison on this branch) but the comparison run was invalidated, so no RPS/p95 numbers are reported: the local-memory after-run served 100% 4xx (the welcome fixture _package was missing while the worktree dist was still being repaired, so its high RPS is the 404 fast-path, not rendered output), and the remaining scenarios ran while builds and test suites shared the box, making their deltas unreliable in the other direction. Evidence for this change therefore stands on the unit tests (upstream link step runs exactly once per key with byte-identical HTML) plus green CI. A clean-room bench:quick on an idle machine remains the right before/after gate if a reviewer wants numbers.

oc-template-handlebars render re-runs utils.validator() +
handlebars.template() on every render because its internal cache never
hits (cache.set omits the value argument, so cache.get misses forever).
Wrap the template-link step registry-side with an LRU-bounded cache keyed
by template key (WeakMap identity fallback when keyless) and serve
oc-client the wrapped module, so linking happens once per component
version regardless of the upstream bug. Fail-open: misses delegate to the
upstream render, preserving its validation/error semantics.
…ache

Custom/test-double template modules may expose only getCompiledTemplate
(no getInfo/render). Passing such a module into oc-client breaks its
uniqTemplates (reads getInfo().type), so pass them through unwrapped and
skip Client injection unless the module satisfies oc-client's contract.
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