Just published Haml.cr: GitHub - compumike/haml.cr: Haml templating engine for Crystal · GitHub because I like using Haml on the Rails side ![]()
Thanks Mike!
Some small performance gains you could get:
Have Attributes write directly to the IO instead of building a Hash and then serialising it.
write_preserved is allocating a String just to then gsub it, which will allocate again. You could do this in one pass, I think.
downcase allocates a new string on each compare, use String#compare with case_insensitive: true for a zero-allocation comparison, eg:
VOID_TAGS.any? do |tag|
tag.compare(name, case_insensitive: true).zero?
end
Nothing major, it’s just a passion of mine.
More interestingly, I noticed that valid_attribute_name? was spending a lot of time in String#includes?. It turns out that using a string literal to check for a character is 3x slower than the equivalent hand-written conditionals. Here’s a little benchmark:
require "benchmark"
target = "abcdef".chars.sample
Benchmark.ips do |x|
x.report("string") {
"abcdef".includes? target
}
x.report("explicit") {
next true if 'a' == target
next true if 'b' == target
next true if 'c' == target
next true if 'd' == target
next true if 'e' == target
next true if 'f' == target
}
x.report("list") {
['a', 'b', 'c', 'd', 'e', 'f'].includes? target
}
x.report("tuple") {
{'a', 'b', 'c', 'd', 'e', 'f'}.includes? target
}
end
Results (11th Gen Intel(R) Core™ i5-1135G7 @ 2.40GHz if you must know)
string 257.82M ( 3.88ns) (± 1.19%) 0.0B/op 3.66× slower
explicit 943.95M ( 1.06ns) (± 2.70%) 0.0B/op 1.00× slower
list 64.27M ( 15.56ns) (± 0.71%) 64.0B/op 14.70× slower
tuple 944.58M ( 1.06ns) (± 1.22%) 0.0B/op fastest
Pretty cool. Reminded me a bit of Slang: GitHub - jeromegn/slang: Slim-inspired templating language for Crystal · GitHub
Thanks for the performance suggestions @willhbr! I will take a look when I get some free time! Those look like some quick wins. It’s a passion of mine too.
Reduce intermediate allocations, write directly → faster and reduced GC activity as well.
Yes @renich I was previously using Slang for a little project on the Crystal side, but have been using Haml for over a decade on the Rails side and it was tough for me to keep switching between the two! ![]()
Released haml.cr v1.1.0 with performance optimizations.
The big one is that many simple literals for HTML attributes can be compiled and escaped at compile time, rather than needing a Haml::Runtime::Attributes instance at runtime.
With local_tmp/test_with_attrs.html.haml:
%p None
%p.fw-bold class only
%p#me.fw-bold class and id
%p#me.fw-bold{ title: "hi" } class and id and attr
Rendering into a Crystal macro with hamlc --no-locations local_tmp/test_with_attrs.html.haml yields:
BEFORE: (haml.cr v1.0.3)
__haml_io << "<p>None</p>\n<p class=\"fw-bold\">class only</p>\n<p id=\"me\" class=\"fw-bold\">class and id</p>\n<p"
__haml_0ebfd0ebe86f_1 = ::Haml::Runtime::Attributes.new
__haml_0ebfd0ebe86f_1.add("id", "me")
__haml_0ebfd0ebe86f_1.add("class", "fw-bold")
__haml_0ebfd0ebe86f_1.add("title", ("hi"
))
__haml_0ebfd0ebe86f_1.write_to(__haml_io)
__haml_io << ">class and id and attr</p>\n"
nil
AFTER: (haml.cr v1.1.0)
__haml_io << "<p>None</p>\n<p class=\"fw-bold\">class only</p>\n<p id=\"me\" class=\"fw-bold\">class and id</p>\n<p id=\"me\" class=\"fw-bold\" title=\"hi\">class and id and attr</p>\n"
nil
This speeds up the common case where we’re using the HAML {...} hash syntax to add static attributes to an HTML element which are entirely known at compile-time.
Anything that is a non-trivial expression, such as something with string interpolation (really anything at all besides a double-quoted string, integer, bool, nil) still gets the runtime behavior, which was also made more efficient through some of the other small optimizations.