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

作者:不知先生 来源:简书 发布时间:2021-12-30 查看数:7479

说明

在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 |

结语

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

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