Download Latest Version 2.4.3 source code.zip (413.5 kB)
Email in envelope

Get an email when there's a new version of Handlebars.Net

Home / 2.4.3
Name Modified Size InfoDownloads / Week
Parent folder
2.4.3 source code.tar.gz 2026-08-09 216.3 kB
2.4.3 source code.zip 2026-08-09 413.5 kB
README.md 2026-08-09 3.9 kB
Totals: 3 Items   633.7 kB 0

A performance-focused release: rendering is up to 36% faster with up to 56% fewer allocations, and template compilation is up to 88% faster. No API or behavior changes — all improvements are behavior-preserving and were validated against the full test suite (1912 tests) and A/B benchmark runs at every step. (#667, [#668])

Performance

Rendering — up to 36% faster, up to 56% fewer allocations (#667)

Suite 2.4.2 2.4.3 Time Alloc
RenderToString clean 10.49 µs / 30.2 KB 7.48 µs / 13.4 KB −29% −56%
RenderToString html 13.15 µs / 33.4 KB 10.37 µs / 16.7 KB −21% −50%
RenderList 100 (object) 18.95 µs 12.16 µs −36%
RenderList 1000 (dictionary) 191.3 µs 126.7 µs −34%
RenderNested 20 (object) 19.18 µs 12.44 µs −35%
RenderSimple (object) 636 ns 431 ns −32%
EndToEnd 26.1 µs 23.5 µs −10%

What changed:

  • Cache helper-resolver presence in the late-bind descriptors. ObservableList<T>.Count acquires a ReaderWriterLockSlim per call, and every simple {{name}} paid it once per render — loops paid hundreds of lock acquisitions per render. The descriptors now subscribe once to the append-only resolver list and keep a flag; resolvers registered after compile still take effect.
  • Retain up to 32K chars in the pooled ReusableStringWriter. Outputs over 4096 chars discarded the pooled writer every render, re-growing a fresh StringBuilder(16) chunk by chunk — most of RenderToString's allocations.
  • Monomorphic descriptor cache on ChainSegment. Dotted member access re-resolved the instance's ObjectDescriptor through the ambient context + type-keyed lookup on every segment per render; each segment now holds an immutable (factory, version, type) → descriptor entry, self-invalidated via a version stamp when descriptor providers are registered.
  • Skip the frame-helper cascade walk when no frame-local helpers exist (decorator / in-render registration is tracked per frame).
  • Skip the ConditionalWeakTable probe in SafeStrings until a safe-marked string is ever produced.
  • Read ThrowOnUnresolvedBindingExpression only on the unresolved branch of path resolution.
  • Cheaper falsy checks: typed zero comparisons instead of Convert.ToBoolean dispatch, an O(1) ICollection.Count emptiness fast path, and enumerator disposal in Any().

Compilation — up to 88% faster (#668)

Benchmark 2.4.2 2.4.3 Change
Compilation (nested 3-level template) 10.80 ms 1.85 ms −83%
CompileMany N=10 57.7 ms 6.97 ms −88%
CompileMany N=100 537.9 ms 78.1 ms −85%

Template compile time turned out to be dominated by the JIT compiling each template's dynamic method at CreateDelegate, inline-expanding the [AggressiveInlining] write/encoder machinery into every mustache call site of every template. The compiler now emits thin NoInlining static entry points that are JIT-compiled once per process. Render performance was verified unchanged by A/B guardrail benchmarks (the one variant that cost ~4% at render time was rejected and kept inline).

Compatibility notes

  • No public API changes; all new members are internal. Template semantics are unchanged, including late registration of helpers, helper resolvers, and descriptor providers after compile.
  • The ChainSegment descriptor cache allocates a small entry (~48 B) when a segment observes a new instance type; heterogeneous collections rendered through dotted access can re-allocate per type flip, while the common homogeneous case allocates once per segment ever.

Contributors

@rexm

Full Changelog: https://github.com/Handlebars-Net/Handlebars.Net/compare/2.4.2...2.4.3

Source: README.md, updated 2026-08-09