Skip to content

Commit b7f1801

Browse files
feat(template): add render_once() for optimized single-render scenarios
Add render_once() method that takes ownership of the schema instead of cloning it, providing significant performance improvements for large schemas (up to 10x faster for 1000+ keys). - Add render_once() public method with comprehensive documentation - Add init_render_once() internal helper using mem::take - Add unit tests for render_once() functionality - Add performance comparison benchmarks - Document usage guidelines and performance characteristics in README This is ideal for web applications where templates are rendered once per request and then discarded.
1 parent af9732b commit b7f1801

6 files changed

Lines changed: 2779 additions & 3 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ The IPC approach introduces performance overhead due to inter-process communicat
8585

8686
For most web applications, the security and interoperability benefits compensate for the performance overhead.
8787

88+
### **Optimizing First Render (Large Schemas):**
89+
90+
When working with large schemas (thousands of keys), the first render can be optimized using `render_once()` instead of `render()`. This method takes ownership of the schema instead of cloning it, providing significant performance improvements:
91+
92+
| Schema Size | render() | render_once() | Speedup |
93+
|-------------|----------|---------------|---------|
94+
| 100 keys | 0.09 ms | 0.02 ms | ~3.7x |
95+
| 500 keys | 0.32 ms | 0.05 ms | ~7x |
96+
| 1000 keys | 0.65 ms | 0.06 ms | ~10x |
97+
| 2000 keys | 1.15 ms | 0.10 ms | ~11x |
98+
99+
**When to use `render_once()`:**
100+
- Single render per template instance (most common use case)
101+
- Large schemas with thousands of keys
102+
- Memory-constrained environments
103+
104+
**When NOT to use `render_once()`:**
105+
- Multiple renders from the same template instance
106+
- Template reuse scenarios
107+
108+
After `render_once()`, the template cannot be reused because the schema is consumed. Use `render()` for reusable templates.
109+
88110
### **IPC Components:**
89111
- **IPC Server**: Universal standalone application (written in Rust) for all languages - download from: [IPC Server](https://github.com/FranBarInstance/neutral-ipc/releases)
90112
- **IPC Clients**: Language-specific libraries to include in your project - available at: [IPC Clients](https://github.com/FranBarInstance/neutral-ipc/tree/master/clients)

src/template.rs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,90 @@ impl<'a> Template<'a> {
311311
self.out.clone()
312312
}
313313

314-
// Restore vars for render
314+
/// Renders the template content without cloning the schema.
315+
///
316+
/// This is an optimized version of `render()` that takes ownership of the schema
317+
/// instead of cloning it. Use this when you only need to render once per template
318+
/// instance, which is the most common use case in web applications.
319+
///
320+
/// # When to Use
321+
///
322+
/// - **Single render per request**: Most web applications create a template, render it once,
323+
/// and discard it. This is the ideal use case for `render_once()`.
324+
/// - **Large schemas**: When your schema contains thousands of keys, the performance
325+
/// improvement can be 5-10x faster than `render()`.
326+
/// - **Memory-constrained environments**: Avoids the memory spike of cloning large schemas.
327+
///
328+
/// # When NOT to Use
329+
///
330+
/// - **Multiple renders**: If you need to render the same template multiple times with
331+
/// the same schema, use `render()` instead.
332+
/// - **Template reuse**: After `render_once()`, the template cannot be reused because
333+
/// the schema is consumed.
334+
///
335+
/// # Performance
336+
///
337+
/// Benchmarks show significant improvements for large schemas:
338+
/// - 100 keys: ~3.7x faster
339+
/// - 500 keys: ~7x faster
340+
/// - 1000+ keys: ~10x faster
341+
///
342+
/// # Post-Call Behavior
343+
///
344+
/// After calling this method, the template's schema will be empty (`{}`) and subsequent
345+
/// calls to `render()` or `render_once()` will produce empty output for schema variables.
346+
/// The template struct itself remains valid but should be discarded after use.
347+
///
348+
/// # Example
349+
///
350+
/// ```rust
351+
/// use neutralts::Template;
352+
///
353+
/// let schema = serde_json::json!({
354+
/// "data": {
355+
/// "title": "Hello World"
356+
/// }
357+
/// });
358+
///
359+
/// let mut template = Template::new().unwrap();
360+
/// template.merge_schema_value(schema);
361+
/// template.set_src_str("{:;title:}");
362+
///
363+
/// // Single render - use render_once() for best performance
364+
/// let output = template.render_once();
365+
/// assert!(output.contains("Hello World"));
366+
///
367+
/// // Template should NOT be reused after render_once()
368+
/// // Create a new Template instance for the next render
369+
/// ```
370+
///
371+
/// # Returns
372+
///
373+
/// The rendered template content as a string.
374+
pub fn render_once(&mut self) -> String {
375+
// Fast path: when there are no blocks, skip full render initialization.
376+
self.time_start = Instant::now();
377+
if !self.raw.contains(BIF_OPEN) {
378+
self.out = self.raw.trim().to_string();
379+
self.time_elapsed = self.time_start.elapsed();
380+
return self.out.clone();
381+
}
382+
383+
let inherit = self.init_render_once();
384+
self.out = BlockParser::new(&mut self.shared, inherit.clone()).parse(&self.raw, "");
385+
386+
while self.out.contains("{:!cache;") {
387+
let out;
388+
out = BlockParser::new(&mut self.shared, inherit.clone()).parse(&self.out, "!cache");
389+
self.out = out;
390+
}
391+
392+
self.ends_render();
393+
394+
self.out.clone()
395+
}
396+
397+
// Restore vars for render (clones schema for reusability)
315398
fn init_render(&mut self) -> BlockInherit {
316399
self.time_start = Instant::now();
317400
self.shared = Shared::new(self.schema.clone());
@@ -352,6 +435,49 @@ impl<'a> Template<'a> {
352435
inherit
353436
}
354437

438+
// Restore vars for render_once (takes ownership of schema, no clone)
439+
fn init_render_once(&mut self) -> BlockInherit {
440+
self.time_start = Instant::now();
441+
// Take ownership of schema instead of cloning - leaves empty object in place
442+
let schema = std::mem::take(&mut self.schema);
443+
self.shared = Shared::new(schema);
444+
445+
if self.shared.comments.contains("remove") {
446+
self.raw = remove_comments(&self.raw);
447+
}
448+
449+
// init inherit
450+
let mut inherit = BlockInherit::new();
451+
let indir = inherit.create_block_schema(&mut self.shared);
452+
self.shared.schema["__moveto"] = json!({});
453+
self.shared.schema["__error"] = json!([]);
454+
self.shared.schema["__indir"] = json!({});
455+
self.shared.schema["__indir"][&indir] = self.shared.schema["inherit"].clone();
456+
inherit.current_file = self.file_path.to_string();
457+
458+
// Escape CONTEXT values
459+
filter_value(&mut self.shared.schema["data"]["CONTEXT"]);
460+
461+
// Escape CONTEXT keys names
462+
filter_value_keys(&mut self.shared.schema["data"]["CONTEXT"]);
463+
464+
if !self.file_path.is_empty() {
465+
let path = Path::new(&self.file_path);
466+
467+
if let Some(parent) = path.parent() {
468+
inherit.current_dir = parent.display().to_string();
469+
}
470+
} else {
471+
inherit.current_dir = self.shared.working_dir.clone();
472+
}
473+
474+
if !self.shared.debug_file.is_empty() {
475+
eprintln!("WARNING: config->debug_file is not empty: {} (Remember to remove this in production)", self.shared.debug_file);
476+
}
477+
478+
inherit
479+
}
480+
355481
// Rendering ends
356482
fn ends_render(&mut self) {
357483
self.set_moveto();

0 commit comments

Comments
 (0)