Haml.cr: Haml template engine for Crystal

Just published Haml.cr: GitHub - compumike/haml.cr: Haml templating engine for Crystal · GitHub because I like using Haml on the Rails side :smiley:

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. :racing_car: 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! :smiley:

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.