dbt-jinja 基准测试指南:用 Criterion 追踪 minijinja 引擎性能并横向对比主流模板引擎
2026/9/15 5:22:01 网站建设 项目流程

dbt-jinja 基准测试指南:用 Criterion 追踪 minijinja 引擎性能并横向对比主流模板引擎

【免费下载链接】dbtdbt enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.项目地址: https://gitcode.com/GitHub_Trending/db/dbt

导读

本文聚焦于 dbt 开源仓库中 dbt-jinja 项目自带的基准测试套件(位于 crates/dbt-jinja/benchmarks),它既承担着对 minijinja 模板引擎自身的随时间演进追踪(parse / compile / render 三个关键阶段),也负责与handlebarsliquidteraaskamarinja等主流 Rust 模板引擎的横向对比。读完本文,你将掌握如何用一条cargo bench命令复现这套基准、理解每个基准函数在测什么、看懂比较结果数据,并能从源码层面理解为什么 minijinja 在编译与渲染阶段表现出不同特性。

基准测试套件概览

依据 benchmarks/README.md,这是一套「刚刚起步(beginning)」但结构清晰的基准测试套件,目前包含两类目标:

  • 引擎自身基准(engine benchmarks):追踪 minijinja 在解析、编译、渲染等环节的性能随时间的变化,用于防止回归;
  • 对比基准(comparison benchmarks):在相同模板语义下,与handlebarsliquidtera(以及编译期模板askamarinja)做横向对比。

整套基准基于 criterion.rs(Criterion 基准测试框架)实现,它内置统计检验、回归检测和 HTML 报告生成能力,非常适合「追踪随时间变化」这一目标。

运行方式

crates/dbt-jinja/benchmarks目录下(或仓库根目录下使用-p benchmarks指定包)执行:

$ cargo bench

Criterion 会依次运行全部[[bench]]目标,并在完成后输出每个基准的置信区间估计(time: [lower mean upper])。

基准测试的工程配置

从 Cargo.toml 可以完整还原这套基准的构建方式:

[package] name = "benchmarks" version = "0.1.0" edition = "2018" [features] speedups = ["minijinja/speedups"] [dependencies] askama = "0.12.1" handlebars = "5.1.2" liquid = "0.26.1" minijinja = { path = "../minijinja", default-features = false, features = [ "unstable_machinery", "multi_template", "builtins", "serde", ] } rinja = "0.3.4" serde = { workspace = true } tera = "1.17.1" [dev-dependencies] criterion = { version = "0.5.1", features = ["html_reports"] } [[bench]] name = "templates" harness = false [[bench]] name = "comparison" harness = false

几个值得注意的配置点:

  • minijinja以路径依赖引入path = "../minijinja"),即基准直接对本仓库内的 minijinja 源码进行评测,而不是 crates.io 上的发布版本,这正是「追踪引擎自身随时间变化」的前提;
  • 通过 feature 组合unstable_machinerymulti_templatebuiltinsserde显式开启解析器机械结构(machinery)、多模板、内置函数与 Serde 支持;default-features = false则关闭默认特性,保持测试环境的可控性;
  • 单独的speedupsfeature 可一键开启minijinja/speedups(minijinja 的可选速度优化特性),方便对比开启与否的性能差异;
  • 两个[[bench]]目标都声明harness = false,即不使用 Rust 标准测试 harness,改由 Criterion 接管基准调度;
  • criterion开启了html_reportsfeature,运行结束后会在target/criterion/report生成可视化 HTML 报告;
  • askamarinja分别通过 askama.toml 与 rinja.toml 中的dirs = ["inputs"]配置模板搜索目录。

引擎自身基准:parse / compile / render 三阶段追踪

引擎自身基准定义在 benches/templates.rs 中,它使用criterion_main!注册单个criterion_benchmark分组,覆盖 minijinja 的三个核心阶段:

1. parse:纯语法解析

fn do_parse() { parse( black_box(include_str!("../inputs/all_elements.html")), "all_elements.html", Default::default(), Default::default(), ) .unwrap(); }

这里直接调用minijinja::machinery::parse,对模板源码做词法与语法层面的解析(尚未编译成可执行指令),基准名为parseblack_box用于阻止编译器将常量输入优化掉,确保真实测量解析开销。

2. compile:解析并编译

fn do_parse_and_compile() { let mut env = Environment::new(); env.add_template( "all_elements.html", include_str!("../inputs/all_elements.html"), ) .unwrap(); }

compile基准将解析与编译合并为一次Environment::add_template调用,反映模板「注册进环境」这一实际使用场景的开销。

3. render:完整渲染

fn do_render(env: &Environment) { let tmpl = env.get_template("all_elements.html").unwrap(); tmpl.render(context! { DEBUG => false, site => context! { nav => vec![ context!{url => "/", is_active => true, title => "Index"}, context!{url => "/doc", is_active => false, title => "Docs"}, context!{url => "/help", is_active => false, title => "Help"}, ] }, items => (0..200).skip(3).collect::<Vec<_>>(), }) .unwrap(); }

渲染基准通过create_real_env()构建了一个更接近生产环境的实例——额外注册了footer.html子模板、自定义过滤器asset_url与自定义函数current_year

fn create_real_env() -> Environment<'static> { let mut env = Environment::new(); env.add_template("footer.html", include_str!("../inputs/footer.html")) .unwrap(); env.add_template( "all_elements.html", include_str!("../inputs/all_elements.html"), ) .unwrap(); env.add_filter("asset_url", |_: &State, value: String| Ok(value)); env.add_function("current_year", |_: &State| Ok(2022)); env }

渲染上下文包含一个 197 项的列表((0..200).skip(3)),确保基准覆盖真实的循环与过滤链工作量。

测试模板:all_elements.html

inputs/all_elements.html 是这个基准的「全家桶」模板,几乎覆盖了 minijinja 的核心语法元素,这正是命名为 all_elements 的原因:

<!doctype html> <meta charset="utf-8"> <title>{% block title %}{% endblock %} | Hello</title> <link rel="stylesheet" href="{{ 'styles/index.css'|asset_url }}"> {# this is a comment #} <body> {% block html_body %} <header> <h1>Hello</h1> <nav> <ul class="nav"> {% for item in site.nav %} <li><a href="{{ item.url }}"{% if item.is_active %} class="active"{% endif %}>{{ item.title|upper }}</a> {% endfor %} </ul> </nav> </header> <main> {% block body %} <ul> {% for item in items %} <li>{{ loop.index }}: {{ item|upper }}</li> {% endfor %} </ul> {% endblock %} </main> <footer> {% block footer %} {% with copyright=current_year() %} {% include "footer.html" %} {% endwith %} {% endblock %} </footer> {% if DEBUG %} <pre class="debug">{{ debug() }}</pre> {% endif %} {% endblock html_body %} </body>

它一次性覆盖了:块继承({% block %})、注释({# ... #})、for循环与loop.indexif条件、过滤器(|upper|asset_url)、with作用域、include子模板引入、函数调用(current_year()debug())以及site.nav/item.url这类嵌套属性访问。模板中还包含footer.html的 include(定义于 inputs/footer.html),使渲染链路更完整。

跨引擎对比基准:相同语义、不同语法

对比基准定义在 benches/comparison.rs 中,注册了cmp_compilecmp_render两个分组。

统一的基准上下文

为了让对比公平,各引擎共享同一份序列化上下文。Context派生Serialize,内部包含导航列表(site.nav)、版权年份与标题,并通过宏为Context(askama 用)与RinjaContext(rinja 用)生成相同的Default实现:

#[derive(Serialize, Debug, askama::Template)] #[template(path = "comparison/askama.html")] struct Context { items: Vec<String>, site: Site, title: &'static str, }

items为 6 个字符串条目,site.nav为 4 个导航项,其中一个标记为激活态。各引擎的输入模板分布在 inputs/comparison 目录下,包括minijinja.htmltera.htmlliquid.htmlhandlebars.htmlaskama.html及其各自的*_footer.html子模板。这些模板在结构上保持同构(导航循环、条目循环、页脚 include),仅在语法上遵循各引擎方言——例如 liquid 使用{{ item.title|upcase }}{% for %}/{% include %}语法(liquid.html),handlebars 使用{{#each items}}{{> footer.html}}语法(handlebars.html),而 tera 与 minijinja 同为 Jinja 系语法,模板几乎一致(tera.html 与 minijinja.html)。

cmp_compile:编译性能对比

该分组在b.iter内反复向各引擎注册同一对模板(主模板 + footer 子模板):

  • minijinja:每次迭代执行两次env.add_template
  • tera:每次迭代执行两次tera.add_raw_template
  • liquid:每次迭代用ParserBuilder::with_stdlib()解析两次模板;
  • handlebars:每次迭代执行两次hbs.register_template_string

由于 askama/rinja 属于编译期模板(模板在 Rust 编译阶段完成编译),它们不参与编译基准——这一点 README 中也有专门说明。

cmp_render:渲染性能对比

该分组在循环外完成模板注册,b.iter内仅执行渲染,并针对各引擎特点做了等价适配:

  • minijinjaenv.get_template("template.html").unwrap().render(Context::default())
  • tera:先tera::Context::from_serialize(Context::default())tera.render(...)
  • liquid:使用EagerCompiler<InMemorySource>预编译 partials,liquid::to_object转换上下文后渲染;
  • handlebars:通过handlebars_helper!注册upper辅助函数后渲染;
  • rinja / askama:直接调用Template::render(&context)——由于模板是编译期生成的 Rust 代码,渲染路径没有字符串解析与运行时编译开销。

模板语义的对齐细节

对比模板刻意保留了各引擎的惯用写法而非机械翻译:minijinja 与 tera 的模板文件在页脚处都通过{% include "footer.html" %}引入子模板,handlebars 用 partial({{> footer.html}}),liquid 则通过EagerCompiler预编译 partial。这种「语义等价、语法惯用」的设计,使得对比结果反映的是各引擎在真实使用方式下的表现,而非被强行统一语法后的失真数据。

对比结果与解读

README 记录了在MacBook Pro 16"(2021 款)上的一次实测结果(单位为微秒,Criterion 输出置信区间[lower mean upper]):

cmp_compile/handlebars time: [47.043 µs 47.163 µs 47.295 µs] cmp_compile/liquid time: [28.669 µs 28.776 µs 28.919 µs] cmp_compile/minijinja time: [4.6921 µs 4.7003 µs 4.7092 µs] cmp_compile/tera time: [35.145 µs 35.227 µs 35.315 µs] cmp_render/askama time: [1.5744 µs 1.5793 µs 1.5846 µs] cmp_render/handlebars time: [6.3056 µs 6.3221 µs 6.3399 µs] cmp_render/liquid time: [11.364 µs 11.394 µs 11.426 µs] cmp_render/minijinja time: [5.0475 µs 5.0600 µs 5.0739 µs] cmp_render/tera time: [7.1101 µs 7.1285 µs 7.1482 µs]

如何阅读这组数据

  • 编译阶段(cmp_compile):minijinja 约 4.70 µs,明显快于 tera(约 35.2 µs)、liquid(约 28.8 µs)与 handlebars(约 47.2 µs),差距在一个数量级左右。这与 minijinja 把解析与编译分离、且解析器轻量的实现思路一致;
  • 渲染阶段(cmp_render):askama 约 1.58 µs 领先,符合其「编译期模板 + 静态类型」的定位;minijinja 约 5.06 µs,介于 handlebars(约 6.32 µs)与 tera(约 7.13 µs)之间,明显快于 liquid(约 11.4 µs)。

两个必须注意的边界

  1. Askama 没有编译基准。README 明确指出:Askama(以及同类的 rinja)在 Rust 构建阶段就完成了模板编译,并采用静态类型检查,因此它没有cmp_compile数据。换言之,cmp_render中 askama 的领先是以「编译期成本转移到构建过程」为代价的,不能简单解读为「渲染一定更快」;
  2. 数据是特定环境快照。以上数字来自 2021 款 MacBook Pro 16",是某一时刻的样本,不是可移植的性能结论。要复现或验证,请在本仓库中自行运行cargo bench,并结合 Criterion 的target/criterion/reportHTML 报告查看置信区间与回归趋势。

在 dbt 项目中的定位

从仓库结构看,minijinja 是被 dbt 深度依赖的 Jinja 模板引擎实现(minijinja 为该仓库内维护的模板引擎,dbt-jinja家族还包含 dbt-jinja-ctx、dbt-jinja-filters、dbt-jinja-utils、dbt-jinja-vars 等配套 crate)。在这条技术栈上,模板引擎的解析、编译、渲染性能直接影响到大规模 dbt 项目的模型编译与渲染耗时,因此:

  • 引擎自身基准(parse / compile / render)承担回归守护职责,任何一次针对解析器或渲染器的改动,都可以通过cargo bench观察耗时是否出现统计显著的回退;
  • 跨引擎对比基准则作为参考锚点,帮助团队在引入新特性(如speedupsfeature)时量化收益。

总结

dbt-jinja 的 benchmark 套件虽然自称「刚起步」,但已经覆盖了模板引擎最核心的性能维度:

基准分组所在文件测量内容
parse / compile / renderbenches/templates.rsminijinja 引擎自身三阶段耗时
cmp_compilebenches/comparison.rsminijinja vs tera / liquid / handlebars 的模板编译耗时
cmp_renderbenches/comparison.rs上述引擎 + askama / rinja 的渲染耗时

复现与扩展这套基准非常简单:直接运行cargo bench,即可获得含置信区间的统计结果与 HTML 报告。结合 Cargo.toml 中预留的speedupsfeature,你还可以一键对比 minijinja 在开启速度优化前后的差异——这为后续的性能调优工作提供了一个开箱即用的测量起点。

【免费下载链接】dbtdbt enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.项目地址: https://gitcode.com/GitHub_Trending/db/dbt

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询