说明
在github上搜索template engine
,各编程语言搜索结果如下:
-
JavaScript
4185条 -
PHP
1501 条 -
HTML
769 条 -
Java
720 条 -
Python
615条 -
C#
290条
挑选其中语言为C#
的模板引擎,根据star数量进行排序。然后排除已经三年以上未更新的及部分功能上满足不了的模板引擎,最终选择以下几款模板引擎参与测试:
- Handlebars.Net v2.0.9
- fluid v2.2.3
- RazorEngineCore v2021.7.1
- cottle v2.0.4
- jntemplate v2.2.4
- MiniRazor v2.2.0
其中star最高的二款RazorEngine,RazorLight因为已经多年未再进行更新被排除,挑选了同样是基于Razor的RazorEngineCore与MiniRazor参与测试。
测试环境
操作系统: Windows 7 SP1 (6.1.7601.0)
测试工具: BenchmarkDotNet v0.13.1
.NET Runtime: .NET 5.0.11 (5.0.1121.47308), X64 RyuJIT
说明:Mean表示平均耗时,Allocated表示内存情况,其它各项的意思可自行搜索了解。MS表示毫秒,NS表示是纳秒
测试过程
测试一:
调用对象属性或直接呈现变量,执行100000次
源码如下:
[MemoryDiagnoser]
public class TestVariable
{
private int Max = 100000;
[Benchmark]
public void RunJntemplate()
{
string text = "Hello $model.Id";
var template = Engine.CreateTemplate(text.GetHashCode().ToString(), text);
for (var i = 0; i < Max; i++)
{
template.Set("model", new UserInfo { Id = i, Name = "your name!" });
var value = template.Render();
if(value!=$"Hello {i}")
{
throw new Exception("Jntemplate ERROR.");
}
}
}
[Benchmark]
public void RunHandlebars()
{
string source =
@"Hello {{Id}}";
var t = Handlebars.Compile(source);
for (var i = 0; i < Max; i++)
{
var value = t(new UserInfo
{
Id = i,
Name = "your name!"
});
if (value != $"Hello {i}")
{
throw new Exception("Handlebars ERROR.");
}
}
}
[Benchmark]
public void RunRazorEngineCore()
{
string text = "Hello @Model.Id";
RazorEngine razorEngine = new RazorEngine();
var t = razorEngine.Compile(text);
for (var i = 0; i < Max; i++)
{
var value = t.Run(new UserInfo { Id = i, Name = "your name!" });
if (value != $"Hello {i}")
{
throw new Exception("RazorEngineCore ERROR.");
}
}
}
[Benchmark]
public void RunMiniRazor()
{
string text = "Hello @Model.Id";
var t = MiniRazor.Razor.Compile(text);
for (var i = 0; i < Max; i++)
{
var value = t.RenderAsync(new UserInfo { Id = i, Name = "your name!" }).GetAwaiter().GetResult();
if (value != $"Hello {i}")
{
throw new Exception("MiniRazor ERROR.");
}
}
}
[Benchmark]
public void RunFluid()
{
var parser = new Fluid.FluidParser();
var text = "Hello {{ Id }}";
if (!parser.TryParse(text, out var template, out var error))
{
throw new Exception("Fluid ERROR.");
}
for (var i = 0; i < Max; i++)
{
var context = new Fluid.TemplateContext(new UserInfo { Id = i, Name = "your name!" });
var value = template.Render(context);
if (value != $"Hello {i}")
{
throw new Exception("RazorHosting ERROR.");
}
}
}
[Benchmark]
public void RunCottle()
{
var text = "Hello {Id}";
var documentResult = Document.CreateDefault(text); // Create from template string
var template = documentResult.DocumentOrThrow; // Throws ParseException on error
for (var i = 0; i < Max; i++)
{
var context = Cottle.Context.CreateBuiltin(new Dictionary<Value, Value>
{
["Id"] = i,
["Name"] = "your name!"
});
var value = template.Render(context);
if (value != $"Hello {i}")
{
throw new Exception("RazorHosting ERROR.");
}
}
}
}
测试结果如下:
Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Allocated |
---|---|---|---|---|---|---|---|
RunJntemplate | 47.42 ms | 1.165 ms | 3.416 ms | 46.65 ms | 23909.0909 | - | 36 MB |
RunHandlebars | 98.09 ms | 1.914 ms | 4.242 ms | 96.25 ms | 25000.0000 | - | 38 MB |
RunRazorEngineCore | 78.57 ms | 3.684 ms | 10.511 ms | 72.91 ms | 32000.0000 | 2000.0000 | 56 MB |
RunMiniRazor | 309.31 ms | 5.746 ms | 14.729 ms | 304.12 ms | 44000.0000 | 2000.0000 | 76 MB |
RunFluid | 97.29 ms | 2.332 ms | 6.877 ms | 95.48 ms | 34666.6667 | - | 52 MB |
RunCottle | 80.13 ms | 3.234 ms | 9.435 ms | 78.01 ms | 54500.0000 | - | 82 MB |
测试二:
遍历显示一个10万个对象的数组
源码如下:
/// <summary>
/// /
/// </summary>
[MemoryDiagnoser]
public class TestForeach
{
private UserInfo[] arr;
private int max = 100000;
public TestForeach()
{
arr = new UserInfo[max];
for (var i = 0; i < max; i++)
{
arr[i] = new UserInfo { Id = i, Name = $"name{i}" };
}
}
[Benchmark]
public void RunJntemplate()
{
string text = @"
<ul>
$for(node in list)
<li>$node.Id</li>
$end
</ul>
";
var hashCode = text.GetHashCode().ToString();
var template = JinianNet.JNTemplate.Engine.CreateTemplate(hashCode, text);
template.Context.OutMode = OutMode.Auto;
template.Set("list", arr);
var value = template.Render();
//Console.WriteLine(value);
}
[Benchmark]
public void RunHandlebars()
{
var source = @"
<ul>
{{#list}}
<li>{{id}}</li>
{{/list}}
</ul>
";
var t = Handlebars.Compile(source);
var value = t(new
{
list = arr
});
//Console.WriteLine(value);
}
[Benchmark]
public void RunRazorEngineCore()
{
var razorEngine = new RazorEngineCore.RazorEngine();
var TemplateCache = new ConcurrentDictionary<int, IRazorEngineCompiledTemplate>();
string text = @"
<ul>
@{
foreach (var item in Model)
{
<li>@item.Id</li>
}
}
</ul>
";
var template = razorEngine.Compile(text);
var value = template.Run(arr);
//Console.WriteLine(value);
}
[Benchmark]
public void RunMiniRazor()
{
string text = @"
<ul>
@{
foreach (var item in Model)
{
<li>@item.Id</li>
}
}
</ul>
";
var t = MiniRazor.Razor.Compile(text);
var value = t.RenderAsync(arr).GetAwaiter().GetResult();
//Console.WriteLine(value);
}
[Benchmark]
public void RunFluid()
{
var parser = new Fluid.FluidParser();
var text = @"
<ul>
{% for i in list %}
<li>{{i}} </li>
{% endfor %}
</ul>
";
if (!parser.TryParse(text, out var template, out var error))
{
throw new Exception("Fluid ERROR.");
}
var context = new Fluid.TemplateContext(new { list = arr.Select(m=>m.Id).ToArray() });
var value = template.Render(context);
//Console.WriteLine(value);
}
[Benchmark]
public void RunCottle()
{
//var text = "Hello {format(date, \"d:yyyy-MM-dd HH:mm:ss\")}";
var text = @"
<ul>
{for i in list:
<li>{i}</li>
}
</ul>";
var documentResult = Document.CreateDefault(text); // Create from template string
var template = documentResult.DocumentOrThrow; // Throws ParseException on error
var context = Cottle.Context.CreateBuiltin(new Dictionary<Value, Value>
{
["list"] = arr.Select(m => (Value)m.Id).ToArray(),
});
var value = template.Render(context);
// Console.WriteLine(value);
}
}
测试结果如下:
Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|
RunHandlebars | 38.45 ms | 0.764 ms | 1.508 ms | 37.74 ms | 2428.5714 | 1142.8571 | 428.5714 | 15 MB |
RunRazorEngineCore | 38.29 ms | 1.569 ms | 4.627 ms | 37.94 ms | 2000.0000 | - | - | 18 MB |
RunMiniRazor | 146.40 ms | 2.810 ms | 7.691 ms | 143.28 ms | 6000.0000 | 2000.0000 | - | 27 MB |
RunFluid | 68.13 ms | 1.356 ms | 3.428 ms | 68.64 ms | 3714.2857 | 2142.8571 | 857.1429 | 21 MB |
RunCottle | 37.85 ms | 0.588 ms | 0.578 ms | 37.59 ms | 2642.8571 | 1642.8571 | 928.5714 | 17 MB |
RunJntemplate | 23.12 ms | 0.461 ms | 0.888 ms | 23.16 ms | 3562.5000 | 2281.2500 | 968.7500 | 21 MB |
测试三:
调用对象的ToString来格式化时间,对于不支持调用函数的引擎,则使用其自带的格式化功能
源码如下:
[MemoryDiagnoser]
public class TestMethod
{
private int Max = 100000;
[Benchmark]
public void RunJntemplate()
{
string text = "Hello $date.ToString(\"yyyy-MM-dd\")";
var template = Engine.CreateTemplate(text.GetHashCode().ToString(), text);
for (var i = 0; i < Max; i++)
{
var date = DateTime.Now;
template.Set("date", date);
var value = template.Render();
if (value != $"Hello {date.ToString("yyyy-MM-dd")}")
{
throw new Exception("Jntemplate ERROR.");
}
//var value = engine.Parse(text, new UserInfo { Id = 10, Name = "your name!" });
}
}
[Benchmark]
public void RunHandlebars()
{
string source =
@"Hello {{date}}";
var format = "yyyy-MM-dd";
var formatter = new CustomDateTimeFormatter(format);
Handlebars.Configuration.FormatterProviders.Add(formatter);
int hashCode = source.GetHashCode();
var t = Handlebars.Compile(source);
for (var i = 0; i < Max; i++)
{
var date = DateTime.Now;
var value = t(new
{
date = date
});
if (value != $"Hello {date.ToString("yyyy-MM-dd")}")
{
throw new Exception("Handlebars ERROR.");
}
}
}
[Benchmark]
public void RunRazorEngineCore()
{
string text = "Hello @Model.ToString(\"yyyy-MM-dd\")";
RazorEngine razorEngine = new RazorEngine();
var t = razorEngine.Compile(text);
for (var i = 0; i < Max; i++)
{
var date = DateTime.Now;
var value = t.Run(date);
if (value != $"Hello {date.ToString("yyyy-MM-dd")}")
{
throw new Exception("RazorEngineCore ERROR.");
}
}
}
[Benchmark]
public void RunMiniRazor()
{
string text = "Hello @Model.ToString(\"yyyy-MM-dd\")";
var t = MiniRazor.Razor.Compile(text);
for (var i = 0; i < Max; i++)
{
var date = DateTime.Now;
var value = t.RenderAsync(date).GetAwaiter().GetResult();
if (value != $"Hello {date.ToString("yyyy-MM-dd")}")
{
throw new Exception("MiniRazor ERROR.");
}
}
}
[Benchmark]
public void RunFluid()
{
var parser = new Fluid.FluidParser();
var text = "Hello {{ date | date: '%F' }}";
if (!parser.TryParse(text, out var template, out var error))
{
throw new Exception("Fluid ERROR.");
}
for (var i = 0; i < Max; i++)
{
var date = DateTime.Now;
var context = new Fluid.TemplateContext(new { date = date });
var value = template.Render(context);
if (value != $"Hello {date.ToString("yyyy-MM-dd")}")
{
throw new Exception("RazorHosting ERROR.");
}
}
}
[Benchmark]
public void RunCottle()
{
//var text = "Hello {format(date, \"d:yyyy-MM-dd HH:mm:ss\")}";
var text = "Hello {myformat(date, \"yyyy-MM-dd\")}";
var documentResult = Document.CreateDefault(text); // Create from template string
var template = documentResult.DocumentOrThrow; // Throws ParseException on error
var f = Value.FromFunction(Function.CreatePure2((state, subject, format) =>
{
return (new DateTime((long)subject.AsNumber)).ToString(format.AsString);
}));
for (var i = 0; i < Max; i++)
{
var date = DateTime.Now;
var context = Cottle.Context.CreateBuiltin(new Dictionary<Value, Value>
{
["myformat"] = f,
["date"] = date.Ticks
});
var value = template.Render(context);
if (value != $"Hello {date.ToString("yyyy-MM-dd")}")
{
throw new Exception("Cottle ERROR.");
}
}
}
}
public sealed class CustomDateTimeFormatter : IFormatter, IFormatterProvider
{
private readonly string _format;
public CustomDateTimeFormatter(string format) => _format = format;
public void Format<T>(T value, in EncodedTextWriter writer)
{
if (!(value is DateTime dateTime))
throw new ArgumentException("supposed to be DateTime");
writer.Write(dateTime.ToString(_format));
}
public bool TryCreateFormatter(Type type, out IFormatter formatter)
{
if (type != typeof(DateTime))
{
formatter = null;
return false;
}
formatter = this;
return true;
}
}
测试结果如下:
Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Allocated |
---|---|---|---|---|---|---|---|
RunJntemplate | 75.95 ms | 1.069 ms | 1.727 ms | 75.42 ms | 27000.0000 | - | 40 MB |
RunHandlebars | 138.34 ms | 2.653 ms | 3.970 ms | 136.74 ms | 28000.0000 | - | 43 MB |
RunRazorEngineCore | 193.16 ms | 4.076 ms | 11.158 ms | 187.68 ms | 40000.0000 | 2000.0000 | 67 MB |
RunMiniRazor | 515.67 ms | 10.290 ms | 23.015 ms | 506.21 ms | 58000.0000 | 2000.0000 | 96 MB |
RunFluid | 166.84 ms | 4.916 ms | 14.493 ms | 161.28 ms | 70333.3333 | - | 105 MB |
RunCottle | 120.18 ms | 4.173 ms | 12.304 ms | 119.04 ms | 62600.0000 | - | 94 MB |
结语
以上测试仅针对部分功能进行测试了,模板引擎涉及到的地方很多,不仅仅包括性能,还包括易用性,使用场景等多方面,仅仅一二个功能用法不能代表不了全部,故以上结果仅供参考。
如果有其它意见欢迎留言。