Jinja2 是 Python 里用得最广的模板引擎,Flask 的页面渲染、Ansible 的任务文件、Sphinx 的文档都是它。语法用 {{ }} 取值,{% %} 写控制流,和 Django 模板、Vue 的模板都很像。
这篇量的是它到底快不快,以及一个更容易被忽略的问题:模板要不要每次重新编译。
最小可用例子
from jinja2 import Environment
TPL = "{% for item in items %}{{ item.name }} ¥{{ '%.2f'|format(item.price) }};{% endfor %}"
tpl = Environment().from_string(TPL)
print(tpl.render(items=[{"name": "键盘", "price": 299}, {"name": "鼠标", "price": 89}]))
from_string 返回的是编译好的模板对象,render 才真正产出字符串。这个区分是后面所有性能结论的前提。
1000 次渲染的耗时
模板是一个 10 条商品的 HTML 列表,含循环、条件判断、循环变量、格式化过滤器。每组跑 1000 次,5 组取中位:
| 写法 | 1000 次耗时 | 单次 |
|---|---|---|
| 模板预编译后复用 | 98.6 毫秒 | 96 微秒 |
| 每次都重新编译 | 3042.7 毫秒 | 3.0 毫秒 |
| 关掉 autoescape | 76.1 毫秒 | 74 微秒 |
| f-string 手工拼 | 4.9 毫秒 | 4.9 微秒 |
| str.format 手工拼 | 9.0 毫秒 | 9.0 微秒 |
编译成本是渲染的 31 倍
表格第三行和第一行的差距,是这篇最该记住的数字:
模板预编译 + 复用 98.6 ms / 1000 次
每次都重新编译 3042.7 ms / 1000 次 → 31.3 倍
写代码时最常见的写法是把模板字符串存在变量里,每次渲染前调用一次 from_string。这在一次渲染的场景下没问题,放进循环就是把这 31 倍的开销重复一千遍。
换个参照更直观:同样是这个东西,每次都重新编译是 3042.7 毫秒,f-string 手工拼是 4.9 毫秒,差 621 倍。也就是说一个渲染一万次的服务,编译方式选错,成本比不用模板引擎还高。
正确做法是把 from_string 的结果缓存下来。用文件加载的话,Environment 自带缓存,get_template 第二次调用拿到的就是编译好的对象,不需要自己管。
autoescape 的意义与代价
默认开启的 autoescape 会把 HTML 特殊字符转义,这是防 XSS 的第一道门:
输入: "a" & <b>
autoescape 开 → "a" & <b>
autoescape 关 → "a" & <b>
注意引号也被转成了 ",不只是尖括号。这个行为在 HTML 属性里是必要的,但同样是它在非 HTML 场景下变成麻烦:渲染 CSV、JSON、邮件正文时开着 autoescape,输出里的引号和 & 全是转义符,下游解析直接报错。
代价是 23% 的耗时:开着 98.6 毫秒,关掉 76.1 毫秒。所以正确的做法不是全局关,而是按模板类型分别建 Environment,HTML 用默认值,纯文本那个显式传 autoescape=False。
什么时候不该用模板引擎
手动拼字符串的那两行数字说明,模板引擎比自己拼慢 20 倍(96 微秒对 4.9 微秒)。它的价值不在速度,在可读性和可维护性:一个两百行的 HTML 页面,用 f-string 拼出来没人愿意改,用模板写谁都看得懂。
输出结构固定、只有几个变量,用 f-string 就够了,多引一个依赖不值。
要生成几十万行报表,先看清瓶颈:如果模板本身简单,96 微秒一次乘以一万次接近 1 秒,这时候该考虑的是流式输出而不是换引擎。
模板少写结束标签会报错。少一个 {% endfor %},from_string 立刻抛 TemplateSyntaxError,消息里会写明找的是哪个结束标签、当前层要闭合的是哪个块,还带行号。这类错误在加载阶段就能发现,不会等到渲染时才炸,这一点比手工拼字符串强很多。
用户提交的模板不能用。模板里能调用属性、能访问对象的方法,把用户输入当模板渲染等于把服务器交出去。要允许用户自定义格式,用受限的字符串替换语法。
没测的部分列一下,3 项:模板继承(extends/include)对编译时间的影响 / 异步渲染 / 与 Mako、Chameleon 的横向对比。








