Skip to content

Performance

Runjucks moves lexing, parsing, and rendering into Rust, then passes the result back to JavaScript. You still pay for JSON serialization of the context on each call and for the N-API boundary, so the wins show up most on CPU-heavy templates (large loops, many interpolations, deep attribute paths), not on tiny one-line snippets.

  • Parsed template caching — When you call renderString with the same source string and compatible environment settings (delimiters, trimBlocks, registered extensions, etc.), the engine reuses the parsed AST instead of lexing and parsing again.
  • Named template caching — With setTemplateMap, templates are cached by name when the map and parse settings are stable; repeated renderTemplate / getTemplate work benefits.
  • Template instancescompile() / new Template() parse once per instance; reuse the instance across many .render(context) calls for the same file.
  • Rust-side hot paths — The core avoids unnecessary work where it can (for example, fast paths for common expression shapes and efficient context lookups). You do not need to configure these; they apply automatically.

The table below is generated from a committed JSON report (docs/src/data/perf/reports/) keyed to the latest entry in index.json. It is not recomputed on every docs CI run.

How to read it: Each case is measured only after Runjucks and Nunjucks produce identical output for that template and context (same fairness setup as perf/run.mjs). nj / rj is mean Nunjucks latency divided by mean Runjucks latency for that case; > 1× means Nunjucks was slower on that row. Figures vary by CPU, OS, and Node version—use them as directional, not a guarantee for your hardware. For methodology and warm vs cold runs, see Measuring below.

Published benchmark snapshot

@zneep/runjucks0.1.8 vs nunjucks 3.2.4. Mode: warm ( one Environment per case; parsed templates cached ). Recorded 2026-03-29T16:31:02.613Z on darwin/arm64, v25.6.1.

Average nunjucks / runjucks (non-skipped cases): 3.86x — values greater than 1x mean Nunjucks was slower on average for this run.

Per-case mean latency (153 cases)
CaseRunjucks (ms)Nunjucks (ms)nj / rj
synth_plain_small0.00200.00301.48x
synth_plain_large0.01120.165114.77x
synth_many_vars0.04900.12652.58x
synth_for_medium0.09990.09520.95x
synth_filters_chain0.00250.01224.81x
synth_var_trim_upper0.00280.00782.76x
synth_var_trim_capitalize0.00290.00792.72x
synth_if_nested0.00330.01655.06x
synth_nested_for0.00680.01892.77x
synth_long_var_lines0.00920.141115.34x
synth_deep_if_chain0.00350.01534.33x
synth_named_template_interp0.00310.00090.30x
conf:smoke_plain_text0.00200.00301.49x
conf:smoke_variable0.00250.00341.37x
conf:tests_js_defined_false0.00210.00532.54x
conf:tests_js_defined_true_null0.00280.00602.19x
conf:tests_js_not_defined0.00270.00632.33x
conf:tests_js_if_defined_branch0.00210.01205.64x
conf:tests_js_if_defined_with_null0.00260.01204.63x
conf:tests_js_null_is_null0.00200.00502.47x
conf:tests_js_none_is_none0.00200.00502.47x
conf:tests_js_falsy_zero0.00210.00562.71x
conf:tests_js_truthy_pancakes0.00240.00602.50x
conf:tests_js_addition_literal0.00200.00492.37x
conf:tests_js_mul_literal0.00210.00482.35x
conf:tests_js_filter_abs0.00210.00602.94x
conf:tests_js_filter_capitalize0.00220.00572.61x
conf:tests_js_filter_default_undefined0.00220.00743.30x
conf:tests_js_for_batch0.00710.03344.68x
conf:tests_js_set_and_output0.00240.00903.70x
conf:tests_js_number_is_number0.00210.00502.33x
conf:tests_js_string_is_string0.00210.00512.41x
conf:tests_js_equalto0.00210.00723.46x
conf:tests_js_sameas0.00340.00802.38x
conf:tests_js_lower_is_lower0.00210.00522.45x
conf:tests_js_upper_is_upper0.00210.00532.54x
conf:lookup_nested_key0.00340.00601.78x
conf:array_index_zero0.00310.00611.96x
conf:filter_length_array0.00220.00853.92x
conf:if_truthy_branch0.00260.00722.81x
conf:for_simple_sum_style0.00410.01052.57x
conf:expr_inline_if0.00210.00633.07x
conf:jinja_compat_slice_range0.00350.039611.25x
conf:jinja_compat_slice_step0.00350.036210.47x
conf:globals_js_range_stop_join0.00230.00974.12x
conf:globals_js_range_start_stop0.00250.01074.28x
conf:globals_js_range_negative_step0.00250.01244.89x
conf:globals_js_cycler_next0.00280.01876.67x
conf:globals_js_joiner_alternate0.00290.01625.61x
conf:globals_js_range_is_callable0.00210.00532.54x
conf:globals_macro_is_callable0.00220.00934.18x
conf:globals_shadow_range_context0.00250.00381.51x
conf:env_add_global_callable_marker0.00210.00532.52x
conf:regex_literal_test_substring0.00450.01182.64x
conf:is_defined_missing_object_key0.00280.00722.56x
conf:is_defined_array_oob0.00320.01424.40x
conf:filters_not_callable_in_is_tests0.00230.01014.35x
conf:macro_safe_output_not_autoescaped0.00380.02095.45x
conf:tests_js_is_gt_ge_lt_le0.00270.02609.76x
conf:tests_js_is_string_gt_coerce0.00210.00723.38x
conf:tests_js_is_ne0.00230.01355.98x
conf:tests_js_is_ne_no_arg0.00230.01245.43x
conf:tests_js_is_undefined_unbound0.00220.00532.46x
conf:tests_js_is_null_not_undefined0.00250.00542.18x
conf:tests_js_is_escaped_safe0.00220.00833.78x
conf:tests_js_is_not_escaped_raw0.00210.00522.50x
conf:tests_js_is_eq_alias0.00270.01385.04x
conf:tests_js_is_iterable_array_and_object0.00370.01082.93x
conf:tests_js_is_iterable_string0.00250.00542.11x
conf:tests_js_is_mapping0.00350.01083.06x
conf:tests_js_same_var_sameas_object0.00320.00782.43x
conf:tests_js_distinct_objects_equalto0.00400.00802.02x
conf:filters_join_simple0.00240.00913.88x
conf:filters_length_string0.00210.00582.78x
conf:filters_upper0.00210.00562.66x
conf:filters_replace0.00230.00833.58x
conf:filters_replace_max_count0.00240.00933.98x
conf:filters_replace_empty_needle0.00230.00833.63x
conf:filters_random_singleton0.00210.00663.12x
conf:filters_round0.00210.00612.95x
conf:filters_escape_autoescape_off0.00220.00602.68x
conf:filters_safe_with_autoescape0.00220.00753.48x
conf:filters_default_boolean_true0.00210.00843.96x
conf:filters_trim_title0.00250.00883.55x
conf:filters_reverse_string0.00220.00582.72x
conf:filters_first_last0.00230.01576.83x
conf:filters_urlencode_string0.00220.00602.73x
conf:filters_dictsort_keys0.00270.01856.77x
conf:filters_select_odd0.00250.01746.97x
conf:filters_reject_even0.00240.01526.39x
conf:filters_abs_negative_int0.00200.00643.14x
conf:filters_striptags_upstream_preserve0.00430.00962.25x
conf:filters_striptags_upstream_flat0.00370.00721.96x
conf:filters_length_object0.00330.00631.92x
conf:filters_safe_then_escape0.00230.00803.49x
conf:filters_escape_then_safe0.00240.00893.64x
conf:filters_replace_regex_flags0.00760.01772.33x
conf:filters_sum_array0.00220.00904.13x
conf:tag_switch_basic0.00220.01064.72x
conf:tag_for_kv_sorted0.00520.02164.14x
conf:tag_multi_set0.00240.00923.80x
conf:tag_set_block0.00350.00722.04x
conf:tag_raw_braces0.00240.00411.69x
conf:tag_verbatim_braces0.00240.00431.76x
conf:tag_macro_greet0.00320.01645.13x
conf:tag_macro_defaults_kwargs0.00460.03026.56x
conf:tag_filter_block_upper0.00220.00843.89x
conf:tag_switch_case1_only0.00220.01014.52x
conf:tag_for_loop_builtins0.00420.03889.17x
conf:tag_call_macro_caller0.00330.02487.52x
conf:tag_call_caller_positional_args0.00600.04367.21x
conf:tag_include_from_map0.00290.00622.10x
conf:tag_include_nested_map0.00310.00662.11x
conf:tag_include_inner_ignore_missing0.00240.00602.56x
conf:tag_extends_layout_map0.03730.01650.44x
conf:tag_trim_blocks_if0.00240.00783.23x
conf:tag_trim_blocks_for0.00530.01562.94x
conf:tag_lstrip_blocks_if0.00330.01133.46x
conf:tag_trim_lstrip_combined0.00270.00913.33x
conf:tag_trim_blocks_for_loop_items0.00730.04245.81x
conf:tag_trim_blocks_no_var_strip0.00260.00572.16x
conf:tag_custom_delimiters_var0.00290.00732.54x
conf:tag_custom_delimiters_block0.00300.01103.68x
conf:tag_custom_delimiters_for0.00500.01232.49x
conf:tag_custom_delimiters_comment0.00220.00512.35x
conf:tag_for_else_empty0.00280.01334.78x
conf:tag_for_tuple_unpack0.00550.02173.97x
conf:tag_for_loop_length0.00480.01473.06x
conf:tag_for_loop_revindex0.00500.01482.98x
conf:tag_for_loop_last0.00490.01953.99x
conf:tag_set_frame_scoping0.00290.01766.14x
conf:tag_nested_if0.00290.01786.18x
conf:tag_include_ignore_missing0.00210.00753.57x
conf:tag_extends_super0.00360.068819.07x
conf:tag_macro_default_args0.00520.03687.15x
conf:tag_import_macro_ns0.00350.01303.74x
conf:tag_from_import_macro0.00360.01153.23x
conf:tag_import_top_level_set0.00280.01013.65x
conf:tag_import_with_context_macro0.00450.01312.93x
conf:tag_import_without_context_macro0.00460.01242.70x
conf:tag_from_import_set_and_macro0.00360.01333.64x
conf:tag_import_macro_is_callable0.00280.01214.24x
conf:tag_filter_block_lower0.00220.00863.95x
conf:tag_inline_if_expr0.00260.00732.76x
conf:tag_inline_if_false0.00260.00732.76x
conf:tag_whitespace_trim_open_close0.00260.00652.53x
conf:tag_comment_removed0.00200.00522.58x
conf:tag_trim_blocks_comment_preserves_newline0.00200.00522.56x
conf:tag_trim_blocks_endraw_preserves_newline0.00200.00562.76x
conf:tag_import_multi_target_export0.00330.01514.63x
conf:tag_import_block_set_export0.00310.01314.29x
conf:tag_from_import_multi_and_block0.00350.01434.08x
conf:tag_import_chained_top_level_sets0.00300.01013.40x
Skipped cases
  • conf:tag_include_without_context_divergent — compareWithNunjucks false (runjucks-only golden; no nunjucks perf baseline)
  1. Reuse environments and templates — Prefer keeping a long-lived Environment and reusing Template objects instead of parsing the same string on every request.
  2. Ship a release native addon in production — Build with npm run build (release). Debug builds are much slower; see Development.
  3. Keep contexts JSON-friendly — The context is converted to JSON-compatible values for the engine. Very large or deeply nested objects cost more to cross the boundary and to look up; structure data to match what templates actually read.
  4. Optional JSON-string ingress — If profiling shows that JSON serialization of the context is a bottleneck, you can pass a JSON string (renderStringFromJson) instead of a live object so Rust parses the payload once. Advanced builds can enable a faster JSON parser in the native addon; see RUNJUCKS_PERF.md in the repository.
  5. Compare fairly vs Nunjucks — Microbenchmarks differ by workload; Runjucks aims to improve worst-case CPU-heavy rows. Treat numbers from npm run perf as directional, not a universal speedup factor.
  • Node vs Nunjucks — From the package root: npm run perf (optional npm run perf:cold for a cold Runjucks environment each iteration). See perf/README.md in the repo. npm run perf:json writes perf/last-run.json including runjucksVersion, nunjucksVersion, mode (warm / cold), and platform metadata—copy into docs/src/data/perf/reports/<version>.json when refreshing the site snapshot (RELEASING.md).
  • Large vs small context (Runjucks only)npm run perf:context after npm run build compares the same template with a minimal context and a large unused nested object, so you can see whether end-to-end time is sensitive to JSON marshalling. Documented in perf/README.md.
  • Rust-only render costcargo bench -p runjucks_core --bench render_hotspots from the native/ directory (or npm run bench:rust from the package root). Steady-state runs mostly measure evaluation after the parse cache is warm.
  • Rust-only parse costcargo bench -p runjucks_core --bench parse_hotspots (or npm run bench:rust:parse) measures lex + parse (cold compilation) and includes a pair comparing full render vs render-only on a pre-parsed AST. See perf/README.md.

Maintainers and contributors: engineering notes, regression list, and backlog live in RUNJUCKS_PERF.md in the repository.