.NET常见模板引擎性能对比测试

不知先生

发表于2021-12-30 15:58:29


说明

在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

结语

以上测试仅针对部分功能进行测试了,模板引擎涉及到的地方很多,不仅仅包括性能,还包括易用性,使用场景等多方面,仅仅一二个功能用法不能代表不了全部,故以上结果仅供参考。


如果有其它意见欢迎留言。


原文转载于简书,原文地址:https://www.jianshu.com/p/1ac648dd341f