Issue #13839 has been updated by jeremyevans0 (Jeremy Evans).
It might be better to compare this to Erubi, the current default ERB template processor in Rails and Tilt. While this approach is fast for small strings, it's actually slower for large strings (probably due to the use of `+=` instead of `<<`). Additionally, in most cases when you are using templates, you want to cache the resulting ruby code so that repeated rendering does not need to reparse the template input. When you start caching the template object, the performance advantage disappears for small strings, and the performance disadvantage becomes larger for large strings.
I don't think ruby should include this this in core or stdlib. I think it would be best to have it as an gem, possibly integrating with Tilt. But I'll admit I'm biased in this respect, as I'm the maintainer of Erubi.
Here's example benchmark code:
~~~ ruby
class Template
attr_reader :input
def initialize(input)
@input = input
end
def output
"output = %\0" + @input.gsub("{%", "\0\n").gsub("%}", "\noutput += %\0") + "\0"
end
def render(binding)
eval(output, binding)
end
end
require 'erubi'
class ErubiTemplate
attr_reader :input
def initialize(input)
@input = input
@src = Erubi::Engine.new(input).src
end
def render(binding)
eval(@src, binding)
end
end
template_input = <<'END'
{% if true %}
Hello #{"World"}
{% end %}
END
erubi_input = <<'END'
<% if true %>
Hello <%= "World" %>
<% end %>
END
require 'benchmark/ips'
Benchmark.ips do |x|
x.report("Template-small"){Template.new(template_input).render(binding)}
x.report("Erubi-small"){ErubiTemplate.new(erubi_input).render(binding)}
x.compare!
end
template = Template.new(template_input)
erubi = ErubiTemplate.new(erubi_input)
Benchmark.ips do |x|
x.report("Template-small-cache"){template.render(binding)}
x.report("Erubi-small-cache"){erubi.render(binding)}
x.compare!
end
template_input *= 10000
erubi_input *= 10000
Benchmark.ips do |x|
x.report("Template-large"){Template.new(template_input).render(binding)}
x.report("Erubi-large"){ErubiTemplate.new(erubi_input).render(binding)}
x.compare!
end
template = Template.new(template_input)
erubi = ErubiTemplate.new(erubi_input)
Benchmark.ips do |x|
x.report("Template-large-cache"){template.render(binding)}
x.report("Erubi-large-cache"){erubi.render(binding)}
x.compare!
end
~~~
and results (slightly reformatted for easier viewing):
~~~
Calculating -------------------------------------
Template-small 19.271k (_ 0.7%) i/s - 97.515k in 5.060508s
Erubi-small 9.123k (_ 0.4%) i/s - 46.163k in 5.060080s
Comparison:
Template-small: 19270.8 i/s
Erubi-small: 9123.2 i/s - 2.11x slower
Calculating -------------------------------------
Template-small-cache 19.962k (_ 0.5%) i/s - 100.980k in 5.058694s
Erubi-small-cache 21.568k (_ 0.5%) i/s - 108.900k in 5.049226s
Comparison:
Erubi-small-cache: 21568.2 i/s
Template-small-cache: 19962.1 i/s - 1.08x slower
Calculating -------------------------------------
Template-large 0.451 (_ 0.0%) i/s - 3.000 in 6.658039s
Erubi-large 0.693 (_ 0.0%) i/s - 4.000 in 5.771604s
Comparison:
Erubi-large: 0.7 i/s
Template-large: 0.5 i/s - 1.54x slower
Calculating -------------------------------------
Template-large-cache 0.449 (_ 0.0%) i/s - 3.000 in 6.682826s
Erubi-large-cache 3.366 (_ 0.0%) i/s - 17.000 in 5.062812s
Comparison:
Erubi-large-cache: 3.4 i/s
Template-large-cache: 0.4 i/s - 7.50x slower
~~~
----------------------------------------
Feature #13839: String Interpolation Statements
https://bugs.ruby-lang.org/issues/13839#change-66283
* Author: se8 (Sbastien Durand)
* Status: Open
* Priority: Normal
* Assignee:
* Target version:
----------------------------------------
Hello!
Here is a KISS implementation of a template engine in Ruby:
~~~ ruby
class Template
attr_reader :input
def initialize(input)
@input = input
end
def output
"output = %\0" + @input.gsub("{%", "\0\n").gsub("%}", "\noutput += %\0") + "\0"
end
def render(binding)
eval(output, binding)
end
end
~~~
Usage:
~~~ text
{% if true %}
Hello #{'World'}
{% end %}
Template.new('...').render(binding)
~~~
It's kind of a hack on top of Ruby string interpolation, so it's hell fast (~4 times faster than ERB).
Could it be a good idea to implement this kind of statements directly in Ruby string interpolation? Maybe a syntax like that:
~~~ text
"%{3.times do}Hello #{'World'}%{end}"
~~~
So Ruby would have a fast minimal native template engine, with #{expressions} and %{statements}:
~~~ text
eval(File.read("..."), binding)
~~~
--
https://bugs.ruby-lang.org/
Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>