| Name | Modified | Size | Downloads / 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>.Countacquires aReaderWriterLockSlimper 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 freshStringBuilder(16)chunk by chunk — most of RenderToString's allocations. - Monomorphic descriptor cache on
ChainSegment. Dotted member access re-resolved the instance'sObjectDescriptorthrough the ambient context + type-keyed lookup on every segment per render; each segment now holds an immutable(factory, version, type) → descriptorentry, 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
ConditionalWeakTableprobe inSafeStringsuntil a safe-marked string is ever produced. - Read
ThrowOnUnresolvedBindingExpressiononly on the unresolved branch of path resolution. - Cheaper falsy checks: typed zero comparisons instead of
Convert.ToBooleandispatch, an O(1)ICollection.Countemptiness fast path, and enumerator disposal inAny().
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
ChainSegmentdescriptor 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