ZipTemplates
ZipTemplates is a tiny, fast templating approach for rendering runtime-specified template strings.
The core idea is simple:
- Parse a template string into two parallel arrays:
statics (the literal parts) and placeholders (the variable slots).
- When rendering, map placeholders to their substituted values, "zip" the placeholders with the statics array, and join the interleaved pieces to produce the final string.
This yields a predictable, low-allocation rendering approach suitable for high-throughput or runtime-driven templating scenarios.
Why ZipTemplates
- Minimal runtime overhead: rendering is a single pass that zips two arrays and joins the pieces.
- Low allocations: avoids building many intermediate strings while concatenating multiple parts.
- Simple to implement and reason about.
- Works well when the template is provided at runtime and templates are simple (interpolation only).
Concept / Contract
- Input: a template string containing placeholder markers (e.g.
{{0}}, $0, or any chosen syntax), and a mapping/array of substitution values to fill placeholders.
- Output: a single rendered string with placeholders replaced by their corresponding values (coerced to strings).
- Errors: if a placeholder index is missing, behavior is either (configurable) to insert an empty string or raise/return an error. The library should document and provide an option for strict mode.
- Complexity: parse O(n) over template length; render O(p) where p is number of placeholders (with a final join cost dependent on output length).
Example (conceptual)
This is a language-agnostic example showing the algorithm.
- Parse a template into
statics and placeholders arrays.
Template: "Hello, {{0}}! You have {{1}} new messages."
- statics => ["Hello, ", "! You have ", " new messages."]
- placeholders => ["0", "1"]
- Render by mapping placeholders to values and zipping:
Values array: ["Alice", 5]
Zipped pieces: ["Hello, ", "Alice", "! You have ", "5", " new messages."]
Join => "Hello, Alice! You have 5 new messages."
Named / nested placeholders example:
Template: "Hi, {{user.name.first}} — balance: {{account.balance}} USD"
- statics => ["Hi, ", " — balance: ", " USD"]
- placeholders => ["user.name.first", "account.balance"]
Values object: { user: { name: { first: 'Sam' } }, account: { balance: 12.34 } }
Resolution: look up each placeholder as a dot-path on the values object:
Zipped pieces: ["Hi, ", "Sam", " — balance: ", "12.34", " USD"]
Join => "Hi, Sam — balance: 12.34 USD"
Minimal API (example signatures)
- parse(template: string) -> { statics: string[], placeholders: string[] }
- render(parsed, values: (string | number | null | undefined)[], {strict?: boolean} = {}) -> string
Notes:
placeholders can be numeric indices or named keys depending on parsing syntax.
- For named placeholders you may pass an object map instead of an array.
Usage (pseudo-JavaScript)
const template = ZipTemplate.parse("Hi, {{name}} — balance: {{balance}} USD");
const out = template.render({ name: "Sam", balance: 12.34 });
console.log(out); // "Hi, Sam — balance: 12.34 USD"
How this compares to common templating solutions
-
Handlebars / Mustache / Nunjucks / EJS
- These are full-featured templating engines with logic (conditionals, loops), helpers, partials, escaping, and more.
- ZipTemplates intentionally focuses only on interpolation. It does not provide logic, control flow, or template helpers.
- Pros vs heavy engines: much smaller runtime, fewer allocations, simpler mental model, faster for plain interpolation.
- Cons vs heavy engines: lacks features like HTML escaping, conditionals, partials, and custom helpers.
-
Native template literals (JS backticks) / string interpolation in other languages
- Native templates are compiled into code at build time or used inline when source code contains the literal templates.
- ZipTemplates is designed for templates that are specified at runtime (e.g., user-provided templates, templates from a database, or dynamically constructed templates).
- Native templates are more ergonomic when templates are static and known at coding time; ZipTemplates shines when templates arrive or change at runtime.
-
Simple concatenation or join
- Manual concatenation is straightforward but can become error-prone and allocate intermediate strings when building larger outputs.
- ZipTemplates reduces allocations by preparing arrays and performing a single join at the end.
-
Performance and memory
- ZipTemplates reduces the number of intermediate string concatenations, which can reduce GC pressure and improve throughput in hot paths that do many renders.
- It’s best to benchmark in your environment. For simple interpolation-only templates, ZipTemplates will usually outperform heavier template engines because it does less work and allocates less.
Theoretical performance calculations
This section gives a compact, language-agnostic view of the costs involved when parsing and rendering with ZipTemplates. It focuses on asymptotic behavior, memory (allocations), and a small worked numeric example you can use to estimate cost for your templates.
- $T$ — template length (characters)
- $p$ — number of placeholders
- $s$ — number of static segments ($s = p + 1$)
- $S_{\mathrm{avg}}$ — average static segment length
- $L_{\mathrm{avg}}$ — average length of resolved placeholder values
- $O$ — total output length
Display formulas (LaTeX):
$$
O = s \cdot S_{\mathrm{avg}} + p \cdot L_{\mathrm{avg}}
$$
$$
\mathrm{Time_{parse}} = O(T)
$$
$$
\mathrm{Time_{render}} = O(p + O)
$$
$$
\mathrm{Space_{aux}} = O(s + p) \quad\text{(plus final output } O(O)\text{)}
$$
Parsing
- Time: O(T). Parsing scans the template once to split it into
statics and placeholders.
- Memory: O(s + p) for the two arrays (number of entries). Each static string is usually a slice/substring of the template (language-dependent); if slicing copies, account for those allocations.
Rendering
- Time: O(p + O). Resolving p placeholders (object lookups or index lookups) and then joining the pieces to produce O characters.
- The cost to convert placeholder values to strings is proportional to the total length of those stringified values (roughly p * L_avg).
- Memory / allocations:
- One array of pieces of size s + p (or equivalent internal buffers) is created.
- The final output string of length O must be allocated once by the runtime (join/concatenate step).
- Per-placeholder temporary string allocations occur if values need coercion to string (depends on runtime/language).
Comparison to common alternatives (rough)
- Repeated concatenation (e.g., building a string by incremental += or via many small concatenations): may cause many intermediate allocations depending on language and runtime. In the worst case, concatenating k parts naively can create O(k) intermediate buffers and lead to extra copying; cost can approach O(k * O) work in badly optimized implementations.
- Heavy template engines: these typically parse into an AST (similar parse cost O(T)) but then execute node-by-node doing more work: condition evaluation, helper calls, escaping, and iteration. That extra work increases CPU cost and allocations proportional to feature usage.
Worked numeric example
- Suppose a template with p = 10 placeholders, s = 11 statics. Let S_avg = 20 chars and L_avg = 8 chars.
- Output length O = 1120 + 108 = 220 + 80 = 300 characters.
- Parsing cost: O(T) (T might be around 300–400 chars depending on placeholder syntax).
- Render cost: resolving 10 placeholders (small), constructing array of 21 pieces, and allocating final string of 300 chars.
- Compared to naive repeated concatenation of 21 pieces, ZipTemplates performs a single final allocation for the joined result and a small array allocation; the naive approach may do several intermediate allocations depending on the runtime.
Practical benchmarking guidance
- Benchmark with realistic templates and value shapes (short vs long substitutions, many vs few placeholders).
- Measure both time and memory/GC allocations. In many languages you can track allocated bytes and number of GC cycles.
- If templates are reused, measure the effect of caching parsed templates (avoid paying parse cost on each render).
Rules of thumb
- For interpolation-only templates: rendering cost is dominated by final output size O and number of placeholders p; ZipTemplates minimizes intermediate allocations by using a single join/concatenate step.
- If you need logic (loops/conditionals) or safe HTML escaping, compare the extra CPU/allocations of a full engine against the development and maintenance costs of implementing those features yourself.
Summary
- Asymptotically, ZipTemplates is optimal for the interpolation-only use case: parse O(T), render O(p + O) time, and a small O(s+p) extra memory for arrays plus one O(O) allocation for the output string.
Fallback (plain text):
- Output length: O = s _ S_avg + p _ L_avg
- Parse time: Time_parse = O(T)
- Render time: Time_render = O(p + O)
- Aux space: Space_aux = O(s + p) + O(O) (final output)
Benchmarks
For small strings on Ryzen 8700GE
| Benchmark |
Time ns (avg) |
flatten json then zip_templates::render |
360.13 |
already flat json zip_templates::render |
36.787 |
zip_templates::render_from_vec_smart |
21.823 |
tera::render |
1090.5 |
mystical_runic::render |
747.51 |
simple_replace |
503.04 |
simple_replace_flat |
181.57 |
Running benches/zip_templates_bench.rs (target/release/deps/zip_templates_bench-4b3e190c814a6892)
Gnuplot not found, using plotters backend
zip_templates::render time: [359.53 ns 360.13 ns 360.85 ns]
change: [−4.1652% −2.7557% −1.0608%] (p = 0.00 < 0.05)
Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
1 (1.00%) high mild
11 (11.00%) high severe
zip_templates::render_flat
time: [36.707 ns 36.787 ns 36.880 ns]
change: [−4.7696% −4.3873% −4.0016%] (p = 0.00 < 0.05)
Performance has improved.
Found 11 outliers among 100 measurements (11.00%)
8 (8.00%) high mild
3 (3.00%) high severe
zip_templates::render_from_vec_smart
time: [21.784 ns 21.823 ns 21.863 ns]
change: [−8.7911% −8.4338% −8.0677%] (p = 0.00 < 0.05)
Performance has improved.
Found 9 outliers among 100 measurements (9.00%)
6 (6.00%) high mild
3 (3.00%) high severe
tera::render time: [1.0891 µs 1.0905 µs 1.0921 µs]
change: [−4.8755% −4.4775% −4.0901%] (p = 0.00 < 0.05)
Performance has improved.
Found 11 outliers among 100 measurements (11.00%)
6 (6.00%) high mild
5 (5.00%) high severe
mystical_runic::render time: [744.88 ns 747.51 ns 750.97 ns]
Found 9 outliers among 100 measurements (9.00%)
2 (2.00%) high mild
7 (7.00%) high severe
simple_replace time: [502.19 ns 503.04 ns 504.08 ns]
change: [−1.6621% −1.2387% −0.8054%] (p = 0.00 < 0.05)
Change within noise threshold.
Found 11 outliers among 100 measurements (11.00%)
4 (4.00%) high mild
7 (7.00%) high severe
simple_replace_flat time: [181.32 ns 181.57 ns 181.86 ns]
change: [+12.015% +12.434% +12.882%] (p = 0.00 < 0.05)
Performance has regressed.
Found 8 outliers among 100 measurements (8.00%)
4 (4.00%) high mild
4 (4.00%) high severe
Trade-offs and when to use
Use ZipTemplates when:
- You only need interpolation (no loops/conditionals/helpers).
- Templates are provided or changed at runtime.
- You care about minimizing allocations and maximizing rendering throughput.
Avoid ZipTemplates when:
- You need escaping, conditionals, loops, or template composition across files.
- You need a mature ecosystem of helpers and tooling (internationalization, partials, etc.).
Edge cases and considerations
- Missing placeholders: decide between empty string substitution vs. throwing an error (provide strict mode).
- Escaping: ZipTemplates does not escape HTML or other contexts by default — callers must escape values where needed.
- Large templates: parsing and storing arrays uses memory proportional to template structure — still usually less allocation-heavy than repeated concatenation.
- Placeholder collision / ambiguous syntax: pick a clear placeholder syntax and document it.
Extensibility
Possible small additions that remain lightweight:
- Strict mode: throw on missing values.
- Named placeholders: support object maps for rendering.
- Caching parsed templates: if templates are reused, store parsed results to avoid reparsing.
Quick checklist for implementers
Files changed / created
README.md — this file: describes the project, usage, and comparisons.
Final notes
ZipTemplates is intentionally small and focused. If you need richer templating features, pair ZipTemplates with a small set of utilities (escaping, caching, and a strict mode) or switch to a full template engine when complexity demands it.