Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Template Engines in Ruby

Template Engines in Ruby

Slides for RubyConf 2014 talk "Template Engines in Ruby" http://rubyconf.org/program#prop_812

Akira Matsuda

November 19, 2014
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. View Slide

  2. <%= self %>

    name: Akira Matsuda

    GitHub: amatsuda

    Twitter: @a_matsuda

    View Slide

  3. View Slide

  4. Rails

    View Slide

  5. Rails

    View Slide

  6. Rails
    Ruby

    View Slide

  7. Rails
    Ruby

    View Slide

  8. Rails
    Ruby
    Haml

    View Slide

  9. Rails
    Ruby
    Haml

    View Slide

  10. Rails
    Ruby
    Haml
    Kaminari

    View Slide

  11. Rails
    Ruby
    Haml
    Kaminari

    View Slide

  12. Rails
    Ruby
    RubyKaigi
    Haml
    Kaminari

    View Slide

  13. Rails
    Ruby
    RubyKaigi
    Haml
    Kaminari

    View Slide

  14. http://rubykaigi.org/2014

    View Slide

  15. http://rubykaigi.org/2014

    The slides are available from the
    "SCHEDULE" link

    The videos are available from the
    "SCHEDULE" link

    Check 'em out!

    View Slide

  16. Rails
    Ruby
    RubyKaigi
    Haml
    Kaminari

    View Slide

  17. Rails
    Ruby
    RubyKaigi
    Haml
    Kaminari
    Asakusa.rb

    View Slide

  18. Rails
    Ruby
    RubyKaigi
    Haml
    Kaminari
    Asakusa.rb

    View Slide

  19. View Slide

  20. begin

    View Slide

  21. ERB
    Part 1

    View Slide

  22. Template Engines in Ruby

    There is the template Engine
    specification in Ruby

    View Slide

  23. Which is called
    "eRuby"

    View Slide

  24. eRuby

    View Slide

  25. eRuby
    Do you know what it is?

    View Slide

  26. eRuby

    Stands for "Embedded Ruby" (≠ mruby)

    The spec for embedding Ruby code into a text
    (mainly HTML file)

    Created by Matz & Shugo Maeda
    (@shugomaeda), who is Matz's boss at their
    company

    Inspired by ePerl spec

    View Slide

  27. eruby

    View Slide

  28. eruby

    The referential implementation of eRuby,
    written in C

    This eruby has a small "r", just as Ruby (=
    language) and ruby(= implementation)

    Created by Shugo Maeda

    No more maintained :<

    View Slide

  29. ERB

    View Slide

  30. ERB

    A Pure Ruby implementation of eRuby

    A ruby stdlib

    Created by Masatoshi Seki (@m_seki), a
    legendary Ruby guru

    Well documented (particularly in
    Japanese)

    View Slide

  31. Erb? ERb? No, ERB!

    I see so many mistakes in books/
    articles/documantations

    It used to be called ERb/ERbLight,
    then renamed to ERB when
    becoming a ruby standard library

    View Slide

  32. Pure Ruby

    Only one file

    lib/erb.rb

    % wc -l
    #=> 1009 LOC

    View Slide

  33. This includes
    documentation, comments

    % grep "^ *#" erb.rb | wc -l
    #=> 517

    So, the code is actually less than
    500 lines

    View Slide

  34. Usage of ERB

    View Slide

  35. Hello!
    puts ERB.new('Hello!').result
    #=> "Hello!"

    View Slide

  36. Embedding Ruby code
    puts ERB.new('1 + 1 = <%= 1 + 1
    %>').result
    #=> "1 + 1 = 2"

    View Slide

  37. Embedding Ruby code
    puts ERB.new('<% if true %>foo<%
    else %>bar<% end %>').result
    #=> "foo"

    View Slide

  38. How does ERB
    work?

    View Slide

  39. ERB#initialize
    class ERB
    def initialize(str, safe_level=nil,
    trim_mode=nil, eoutvar='_erbout')
    ...
    compiler = make_compiler(trim_mode)
    set_eoutvar(compiler, eoutvar)
    @src, @enc = *compiler.compile(str)
    ...
    ennd

    View Slide

  40. ERB#initialize

    Creates a compiler

    The compiler compiles the given
    template into @src

    View Slide

  41. ERB::Compiler#compile
    class ERB
    class Compiler
    def compile(s)
    ...
    out = Buffer.new(self, enc)
    scanner = make_scanner(s)
    scanner.scan do |token|
    case token
    when '<%', '<%=', '<%#'
    add_put_cmd(out, content) if content.size > 0
    ...
    return out.script, enc
    ennnd

    View Slide

  42. ERB::Compiler#compile

    Prepares a buffer

    Scans through the template, splits
    the template into tokens, then
    executes a command for each
    token

    View Slide

  43. add_put_cmd
    class ERB
    class Compiler
    def add_put_cmd(out, content)
    out.push("#{@put_cmd}
    #{content_dump(content)}")
    ennnd

    View Slide

  44. add_put_cmd

    Pushes a Ruby code String into
    the buffer

    @put_cmd => method

    content_dump(content) => params

    View Slide

  45. What is @put_cmd?
    class ERB
    def set_eoutvar(compiler, eoutvar =
    '_erbout')
    compiler.put_cmd = "#{eoutvar}.concat"
    compiler.insert_cmd =
    "#{eoutvar}.concat"
    compiler.pre_cmd = ["#{eoutvar} = ''"]
    compiler.post_cmd =
    ["#{eoutvar}.force_encoding(__ENCODING__)"]
    ennd

    View Slide

  46. @put_cmd

    "_erbout.concat" by default

    View Slide

  47. content_dump
    class ERB
    class Compiler
    def content_dump(s)
    ...
    s.dump
    ...
    ennnd

    View Slide

  48. content_dump

    Just dumps the content

    View Slide

  49. So, add_put_cmd(out, content) is
    equivalent to something like this
    out = Buffer.new(self,
    Encoding::UTF_8)
    out.push("_erbout.concat
    'Hello'.dump")

    View Slide

  50. ERB#result
    class ERB
    def result(b=new_toplevel)
    ...
    eval(@src, b, (@filename ||
    '(erb)'), 0)
    ennnd

    View Slide

  51. ERB internally executes
    something like this
    _erbout = ""
    buf =
    ERB::Compiler::Buffer.new(ERB::Comp
    iler.new(nil))
    buf.push("_erbout.concat
    'Hello'.dump")
    buf.close
    puts eval(buf.script)

    View Slide

  52. How do we
    use ERB?

    View Slide

  53. The major use case

    template file + ruby => HTML

    View Slide

  54. In Ruby on Rails

    A component named ActionView

    View Slide

  55. How does ActionView
    render?
    Class.new(ActionView::Base).new.
    render(inline: "<%= 'Hello' %>")

    View Slide

  56. ActionView::Template#ren
    der
    module ActionView
    class Template
    def render(view, locals, buffer=nil,
    &block)
    ...
    compile!(view)
    view.send(method_name, locals,
    buffer, &block)
    ...
    ennnd

    View Slide

  57. ActionView::Template#co
    mpile!
    module ActionView
    class Template
    def compile!(view)
    return if @compiled
    ...
    mod = view.singleton_class
    ...
    compile(mod)
    ...
    @compiled = true
    ...
    ennnd

    View Slide

  58. ActionView::Template#co
    mpile
    module ActionView
    class Template
    def compile(mod)
    ...
    method_name = self.method_name
    code = @handler.call(self)
    source = <<-end_src
    def #{method_name}(local_assigns, output_buffer)
    _old_virtual_path, @virtual_path = @virtual_path,
    #{@virtual_path.inspect};_old_output_buffer =
    @output_buffer;#{locals_code};#{code}
    ensure
    @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
    end
    end_src
    ...
    mod.module_eval(source, identifier, 0)
    ...
    ennnd

    View Slide

  59. @handler.call(self)

    View Slide

  60. ActionView::Template::Handlers::ERB#
    call
    module ActionView
    class Template
    module Handlers
    class ERB
    def call(template)
    ...
    erb = template_source.gsub(ENCODING_TAG, '')
    ...
    self.class.erb_implementation.new(
    erb,
    :escape => (self.class.escape_whitelist.include?
    template.type),
    :trim => (self.class.erb_trim_mode == "-")
    ).src
    ennnnnd

    View Slide

  61. What ActionView render
    does

    ActionView defines a Ruby
    method on the view object

    The method body is generated by
    ERB.new(template).src

    View Slide

  62. ERB#src

    Returns the Ruby code String to
    be `eval`ed

    View Slide

  63. ERB#result
    class ERB
    def result(b=new_toplevel)
    ...
    eval(@src, b, (@filename ||
    '(erb)'), 0)
    ennd

    View Slide

  64. ERB#src
    ERB.new('Hello').src
    #=> "#coding:UTF-8\n_erbout = '';
    _erbout.concat \"Hello\";
    _erbout.force_encoding(__ENCODING__
    )"

    View Slide

  65. Short summary

    Templates are compiled to be Ruby
    methods

    Only for the first execution of the template

    So, compile overhead is not at all a big deal

    What really matters is performance of the
    generated ruby code

    View Slide

  66. ActionView::Template::Handlers::ERB#
    call
    module ActionView
    class Template
    module Handlers
    class ERB
    def call(template)
    ...
    erb = template_source.gsub(ENCODING_TAG, '')
    ...
    self.class.erb_implementation.new(
    erb,
    :escape => (self.class.escape_whitelist.include?
    template.type),
    :trim => (self.class.erb_trim_mode == "-")
    ).src
    ennnnnd

    View Slide

  67. What is
    self.class.erb_implementation?

    View Slide

  68. ActionView::Template::Handlers::ERB.erb
    _implementation
    module ActionView
    class Template
    module Handlers
    class ERB
    ...
    class_attribute :erb_implementation
    self.erb_implementation = Erubis
    ...
    ennnnd

    View Slide

  69. self.class.erb_implementa
    tion

    Defaulted to Erubis

    View Slide

  70. Erubis

    View Slide

  71. Erubis

    Yet another Pure Ruby implementation of
    eRuby

    A ruby gem

    Created by Makoto Kuwata
    (@makotokuwata)

    https://github.com/kwatch/erubis

    View Slide

  72. According to its README

    "Very fast, almost three times
    faster than ERB"

    "Auto escaping support"

    "Ruby on Rails support"

    ... and so on

    View Slide

  73. I said Erubis is a yet another
    implementation of eRuby, but,

    Both ERB and Erubis are eRuby
    implementations
    #=> false

    Erubis is ERB compatible, plus some
    more original features
    #=> true

    View Slide

  74. Why does
    ActionView use
    Erubis by default?

    View Slide

  75. Wanna use ERB?

    It should be quite easy, it's
    configurable!

    Let's try.

    View Slide

  76. action_view/template/
    handlers/erb.rb
    module ActionView
    class Template
    module Handlers
    class ERB
    ...
    class_attribute :erb_implementation
    - self.erb_implementation = Erubis
    + self.erb_implementation = ::ERB
    ...
    ennnnd

    View Slide

  77. The ActionView tests do
    not pass! :bomb:
    % bundle ex rake
    1530 runs, 3434 assertions, 17
    failures, 29 errors, 1 skips

    View Slide

  78. We've got so many errors
    like this...
    SyntaxError: test template:2:
    syntax error, unexpected ')'
    ...ple.com', :method => :put)
    do ).to_s); _erbout.concat
    "Hello...

    View Slide

  79. This causes a syntax error
    output_buffer = render_erb(
    "<%= form_tag('http://
    www.example.com') do %>Hello world!
    <% end %>")

    View Slide

  80. Because this is not
    allowed in eRuby spec
    <%= form_for @article do |f| %>
    ...
    <% end %>

    View Slide

  81. Do you remember that it had to
    be like this in Rails 1.x and 2.x?
    <% form_for @article do |f| %>
    ...
    <% end %>

    View Slide

  82. How did things change in
    Rails2..Rails3?

    Ask the repo.

    View Slide

  83. Between Rails 2 and 3

    View Slide

  84. A big security
    improvement

    Introducing the SafeBuffer

    Strings are regarded unsafe by
    default

    View Slide

  85. actionpack/lib/action_view/
    template/handlers/erb.rb
    require 'active_support/core_ext/class/attribute_accessors'
    +require 'active_support/core_ext/string/output_safety'
    +require 'erubis'
    module ActionView
    module TemplateHandlers
    + class Erubis < ::Erubis::Eruby
    + def add_preamble(src)
    + src << "@output_buffer = ActionView::SafeBuffer.new;"
    + end
    +
    + def add_text(src, text)
    + src << "@output_buffer << ('" << escape_text(text) << "'.html_safe!);"
    + end
    + ...
    + end
    +
    class ERB < TemplateHandler
    ...
    def compile(template)
    - require 'erb'
    -
    magic = $1 if template.source =~ /\A(<%#.*coding[:=]\s*(\S+)\s*-?%>)/
    erb = "#{magic}<% __in_erb_template=true %>#{template.source}"
    - ::ERB.new(erb, nil, erb_trim_mode, '@output_buffer').src
    + Erubis.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src
    ennnnnd

    View Slide

  86. /ERB/Erubis/

    Why Erubis?

    Because Erubis had the "Auto
    escaping support"

    View Slide

  87. Do you remember that Rails templates
    were not html_safe by default before
    this change?

    We were required to html_escape
    everything manually like this

    <%=h @user.name %>

    View Slide

  88. Then,

    View Slide

  89. Stopped hardcoding
    "Erubis"

    View Slide

  90. actionpack/lib/action_view/
    template/handlers/erb.rb
    module ActionView
    module TemplateHandlers
    class ERB < TemplateHandler
    ...
    + cattr_accessor :erubis_implementation
    + self.erubis_implementation = Erubis
    def compile(template)
    magic = $1 if template.source =~ /\A(<%#.*coding[:=]\s*(\S+)
    \s*-?%>)/
    erb = "#{magic}<% __in_erb_template=true %>#{template.source}"
    - Erubis.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src
    +
    self.class.erubis_implementation.new(erb,
    :trim=>(self.class.erb_trim_mode == "-")).src
    ennnnd

    View Slide

  91. Renamed erubis_implementation
    to erb_implementation

    View Slide

  92. actionpack/lib/action_view/
    template/handlers/erb.rb
    module ActionView
    module TemplateHandlers
    class ERB < TemplateHandler
    ...
    - cattr_accessor :erubis_implementation
    - self.erubis_implementation = Erubis
    + cattr_accessor :erb_implementation
    + self.erb_implementation = Erubis
    def compile(template)
    ...
    - result =
    self.class.erubis_implementation.new(erb,
    :trim=>(self.class.erb_trim_mode == "-")).src
    + result =
    self.class.erb_implementation.new(erb, :trim=>(self.class.erb_trim_mode
    == "-")).src
    ...
    ennnnd

    View Slide

  93. So,

    Rails' default template handler is Erubis, not
    ERB

    Because Erubis has some original extensions
    in addition to eRuby, and the Rails team liked
    it

    We cannot switch erb_implementation back
    to ERB

    View Slide

  94. I noticed this 2 years ago,

    And asked Seki-san if we could
    extend ERB to be "Rails
    compatible" before Ruby 2.0
    release

    View Slide

  95. He tried,

    View Slide

  96. Firstly he tried to add some
    extension points to ERB
    git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/
    trunk@38186
    b2dd03c8-39d4-4d8f-98ff-823fe69b080e
    ---
    ChangeLog | 5 +++++
    lib/erb.rb | 36 ++++++++++++++++++++----------------
    2 files changed, 25 insertions(+), 16 deletions(-)

    View Slide

  97. But gave up.

    Because he didn't like this part in ActionView + Erubis

    BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/

    This code scans the template with the Regexp and
    detects the Ruby block, but this kind of code could be
    imperfect

    So this is not acceptable as an ERB spec, said Seki-
    san.

    View Slide

  98. So what is BLOCK_EXPR?

    And why is this problematic?

    Let's carefully take a look at the
    Regexp.

    View Slide

  99. action_view/template/
    handlers/erb.rb
    module ActionView
    class Template
    module Handlers
    class Erubis < ::Erubis::Eruby
    ...
    BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
    def add_expr_literal(src, code)
    flush_newline_if_pending(src)
    if code =~ BLOCK_EXPR
    src << '@output_buffer.append= ' << code
    else
    src << '@output_buffer.append=(' << code << ');'
    end
    ennnnnd

    View Slide

  100. BLOCK_EXPR

    The Regexp scans the given String token
    and detects if the token opens a block or
    not.

    <%= form_tag('http://
    www.example.com') do %>

    This does match the expression.

    View Slide

  101. How about these blocks?
    BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
    p BLOCK_EXPR =~ "foo('bar') do"
    p BLOCK_EXPR =~ "foo('bar') {"
    p BLOCK_EXPR =~ "foo('bar')do"
    p BLOCK_EXPR =~ "foo('bar'){"

    View Slide

  102. OMG
    BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
    p BLOCK_EXPR =~ "foo('bar') do" #=> matches
    p BLOCK_EXPR =~ "foo('bar') {" #=> matches
    p BLOCK_EXPR =~ "foo('bar')do" #=> does not match!
    p BLOCK_EXPR =~ "foo('bar'){" #=> does not match!

    View Slide

  103. I think I found a bug

    form_for('foo'){...} is a valid Ruby
    code that calls a method with a block

    But ActionView doesn't regard this as
    a block

    And causes a syntax error

    View Slide

  104. But this should be the fix,
    obviously.
    -BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
    +BLOCK_EXPR = /\s*(do|\{)(\s*\|[^|]*\|)?\s*\Z/
    p BLOCK_EXPR =~ "foo('bar') do" #=> matches
    p BLOCK_EXPR =~ "foo('bar') {" #=> matches
    p BLOCK_EXPR =~ "foo('bar')do" #=> matches
    p BLOCK_EXPR =~ "foo('bar'){" #=> matches

    View Slide

  105. Seems good?

    View Slide

  106. Not really :<
    -BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
    +BLOCK_EXPR = /\s*(do|\{)(\s*\|[^|]*\|)?\s*\Z/
    p BLOCK_EXPR =~ "foo('bar') do" #=> matches
    p BLOCK_EXPR =~ "foo('bar') {" #=> matches
    p BLOCK_EXPR =~ "foo('bar')do" #=> matches
    p BLOCK_EXPR =~ "foo('bar'){" #=> matches
    p BLOCK_EXPR =~ "@todo" #=> matches!

    View Slide

  107. If I change the BLOCK_EXPR
    to /\s*(do|\{)(\s*\|[^|]*\|)?\s*\Z/

    <%= @todo %>

    Now ActionView regards this code
    as a block call!

    View Slide

  108. So,

    The block detection code /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ in
    ActionView fails to detect some valid Ruby block calls

    I would fix this particular case

    Maybe like this?

    /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/

    But essentially, this approach is wrong!

    Having such a Ruby parsing code other than the Ruby
    parser would be a cause of such a weird bug

    View Slide

  109. The Ruby parser is a
    nightmare.
    (@nobu)
    Let us keep his words in
    mind.

    View Slide

  110. That is why this syntax is
    not acceptable in ERB
    <%= form_for @article do |f| %>
    ...
    <% end %>

    View Slide

  111. You see how ERB works?

    View Slide

  112. Anyway,

    "ERB or Erubis" is not a big deal

    We don't use neither of them after
    all :trollface:

    View Slide

  113. And our choice is
    of course,

    View Slide

  114. Haml
    Part 2

    View Slide

  115. What is Haml?

    A "templating haiku"

    "well-indented"

    Implemented in Ruby

    Created in May 2006

    http://haml.info/

    View Slide

  116. Usage of Haml

    View Slide

  117. Hello!
    Haml::Engine.new("= 'Hello!'").render
    #=> "Hello!\n"

    View Slide

  118. Generating Ruby code
    Haml::Engine.new("='Hello!'").
    compiler.
    precompiled_with_ambles([])

    View Slide

  119. Demo

    View Slide

  120. Generated Ruby code
    puts Haml::Engine.new("=
    'Hello!'").compiler.precompiled_with_ambles([])
    #=> begin;extend Haml::Helpers;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base",
    "basefont", "br", "col", "command", "embed", "frame", "hr", "img",
    "input", "isindex", "keygen", "link", "menuitem", "meta", "param",
    "source", "track", "wbr"], :preserve=>["textarea", "pre",
    "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5,
    :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true,
    :hyphenate_data_attrs=>true, :cdata=>false});_erbout =
    _hamlout.buffer;@output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.format_script_false_false_fals
    e_false_false_true_false(( 'Hello!'
    ));}\n", 0, false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  121. Hello?

    View Slide

  122. ERB
    ERB.new("<% 'Hello!' %>").src
    #=> "#coding:UTF-8\n_erbout = '';
    'Hello!' ;
    _erbout.force_encoding(__ENCODING__)"

    View Slide

  123. Haml
    puts Haml::Engine.new("=
    'Hello!'").compiler.precompiled_with_ambles([])
    #=> begin;extend Haml::Helpers;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base",
    "basefont", "br", "col", "command", "embed", "frame", "hr", "img",
    "input", "isindex", "keygen", "link", "menuitem", "meta", "param",
    "source", "track", "wbr"], :preserve=>["textarea", "pre",
    "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5,
    :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true,
    :hyphenate_data_attrs=>true, :cdata=>false});_erbout =
    _hamlout.buffer;@output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.format_script_false_false_fals
    e_false_false_true_false(( 'Hello!'
    ));}\n", 0, false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  124. OK, nice haiku.

    View Slide

  125. But seriously,

    We don't usually have to care
    what is executed behind

    But isn't this too much?

    It apparently looks slow...

    View Slide

  126. I said, compilation
    overhead doesn't matter

    But the generated Ruby code
    DOES matter

    Yes, it's time to benchmark

    View Slide

  127. Benchmark

    View Slide

  128. Let's start with the
    simplest case

    View Slide

  129. Benchmarking the
    method call
    require 'erb'
    require 'haml'
    require 'benchmark/ips'
    obj = Object.new
    erb_src = ERB.new("<%= 'Hello!' %>").src
    haml_src = Haml::Engine.new("= 'Hello!'").compiler.precompiled_with_ambles([])
    obj.singleton_class.module_eval("def erb() #{erb_src}; end")
    obj.singleton_class.module_eval("def haml() #{haml_src}; end")
    Benchmark.ips do |x|
    x.report('erb') do
    obj.erb
    end
    x.report('haml') do
    obj.haml
    end
    end

    View Slide

  130. Result
    Calculating -------------------------------------
    erb 69.436k i/100ms
    haml 6.263k i/100ms
    -------------------------------------------------
    erb 1.795M (± 2.9%) i/s - 9.027M
    haml 68.364k (± 5.0%) i/s - 344.465k

    View Slide

  131. Result

    Greeting "Hello!" in Haml is
    26x slower than ERB

    But this is just a micro benchmark

    View Slide

  132. As a little bit more
    realistic benchmark

    Let's see actual rendering speed
    via ActionView

    I'm going to skip the template file
    path resolution which is supposed
    to be heavier than printing "Hello!"

    View Slide

  133. Directly `render`ing the
    template
    require 'erb'
    require 'haml'
    require 'action_view'
    require 'action_controller'
    require 'benchmark/ips'
    require 'haml/template'
    view = Class.new(ActionView::Base).new('.')
    erb_handler = ActionView::Template.handler_for_extension('erb')
    erb_template = ActionView::Template.new("<%= 'Hello!' %>", File.expand_path('hello.erb'),
    erb_handler, {})
    # discarding the first execution that runs the template compilation
    puts erb_template.render(view, {})
    haml_handler = ActionView::Template.handler_for_extension('haml')
    haml_template = ActionView::Template.new("= 'Hello!'", File.expand_path('hello.haml'), haml_handler,
    {})
    puts haml_template.render(view, {})
    Benchmark.ips do |x|
    x.report('erb') do
    erb_template.render(view, {})
    end
    x.report('haml') do
    haml_template.render(view, {})
    end
    end

    View Slide

  134. Result
    Calculating -------------------------------------
    erb 7.837k i/100ms
    haml 4.195k i/100ms
    -------------------------------------------------
    erb 93.728k (± 3.5%) i/s - 470.220k
    haml 46.107k (± 2.7%) i/s -
    230.725k

    View Slide

  135. Result

    Haml is still 2x slower than ERB

    In a semi-micro banchmark

    View Slide

  136. Real ActionView rendering

    Calling ActionView::Base#render
    method

    View Slide

  137. Rendering via
    ActionView::Base
    require 'erb'
    require 'haml'
    require 'action_view'
    require 'action_controller'
    require 'benchmark/ips'
    module Rails
    def self.env
    ActiveSupport::StringInquirer.new('production')
    end
    end
    require 'haml/template'
    view = Class.new(ActionView::Base).new('.')
    puts view.render(template: 'hello', handlers: 'erb')
    puts view.render(template: 'hello', handlers: 'haml')
    Benchmark.ips do |x|
    x.report('erb') do
    view.render(template: 'hello', handlers: 'erb')
    end
    x.report('haml') do
    view.render(template: 'hello', handlers: 'haml')
    end
    end

    View Slide

  138. Result
    Calculating -------------------------------------
    erb 1.356k i/100ms
    haml 1.134k i/100ms
    -------------------------------------------------
    erb 14.152k (± 2.4%) i/s - 71.868k
    haml 11.645k (± 2.9%) i/s - 58.968k

    View Slide

  139. Result

    On the real "Hello!" app, an ERB
    template renders 20ʙ% faster
    than a Haml one

    View Slide

  140. Why is Haml
    slow?

    View Slide

  141. Why is Haml slow?

    Let's read through the generated
    haiku

    View Slide

  142. The code
    begin;extend Haml::Helpers;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer, {:autoclose=>["area",
    "base", "basefont", "br", "col", "command", "embed",
    "frame", "hr", "img", "input", "isindex", "keygen",
    "link", "menuitem", "meta", "param", "source", "track",
    "wbr"], :preserve=>["textarea", "pre",
    "code"], :attr_wrapper=>"'", :ugly=>false,
    :format=>:html5, :encoding=>"UTF-8", :escape_html=>false,
    :escape_attrs=>true, :hyphenate_data_attrs=>true,
    :cdata=>false});_erbout = _hamlout.buffer;@output_buffer
    = output_buffer ||= ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.format_script_false_f
    alse_false_false_false_true_false(( 'Hello!'
    ));}\n", 0, false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  143. The code
    begin;extend Haml::Helpers;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base",
    "basefont", "br", "col", "command", "embed", "frame", "hr", "img",
    "input", "isindex", "keygen", "link", "menuitem", "meta", "param",
    "source", "track", "wbr"], :preserve=>["textarea", "pre",
    "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5,
    :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true,
    :hyphenate_data_attrs=>true, :cdata=>false});_erbout =
    _hamlout.buffer;@output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new rescue nil;;
    _hamlout.push_text("#{_hamlout.format_script_false_false_false_fal
    se_false_true_false(( 'Hello!'
    ));}\n", 0, false);
    _erbout;ensure;@haml_buffer = @haml_buffer.upper if
    @haml_buffer;end;
    4&561
    )&--0
    5&"3%08/

    View Slide

  144. The first part
    begin;extend Haml::Helpers;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer, {:autoclose=>["area",
    "base", "basefont", "br", "col", "command", "embed",
    "frame", "hr", "img", "input", "isindex", "keygen",
    "link", "menuitem", "meta", "param", "source",
    "track", "wbr"], :preserve=>["textarea", "pre",
    "code"], :attr_wrapper=>"'", :ugly=>false,
    :format=>:html5, :encoding=>"UTF-8",
    :escape_html=>false, :escape_attrs=>true,
    :hyphenate_data_attrs=>true, :cdata=>false});_erbout
    = _hamlout.buffer;@output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new rescue nil;;

    View Slide

  145. Options

    View Slide

  146. Haml.has_many options
    begin;extend Haml::Helpers;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer, {:autoclose=>["area",
    "base", "basefont", "br", "col", "command", "embed",
    "frame", "hr", "img", "input", "isindex", "keygen",
    "link", "menuitem", "meta", "param", "source",
    "track", "wbr"], :preserve=>["textarea", "pre",
    "code"], :attr_wrapper=>"'", :ugly=>false,
    :format=>:html5, :encoding=>"UTF-8",
    :escape_html=>false, :escape_attrs=>true,
    :hyphenate_data_attrs=>true, :cdata=>false});_erbout
    = _hamlout.buffer;@output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new rescue nil;;

    View Slide

  147. Haml has various options

    But who actually configures Haml?

    Haml buffer can be configured even in
    runtime!

    No. We don't need such a feature.

    Default must be just fine for everyone.

    Let's just remove them.

    View Slide

  148. The code
    begin;extend Haml::Helpers;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer, {:autoclose=>["area",
    "base", "basefont", "br", "col", "command", "embed",
    "frame", "hr", "img", "input", "isindex", "keygen",
    "link", "menuitem", "meta", "param", "source", "track",
    "wbr"], :preserve=>["textarea", "pre",
    "code"], :attr_wrapper=>"'", :ugly=>false,
    :format=>:html5, :encoding=>"UTF-8", :escape_html=>false,
    :escape_attrs=>true, :hyphenate_data_attrs=>true,
    :cdata=>false});_erbout = _hamlout.buffer;@output_buffer
    = output_buffer ||= ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.format_script_false_f
    alse_false_false_false_true_false(( 'Hello!'
    ));}\n", 0, false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  149. - options
    begin;extend Haml::Helpers;_hamlout =
    @haml_buffer =
    Haml::Buffer.new(haml_buffer);_erbout =
    _hamlout.buffer;@output_buffer =
    output_buffer ||=
    ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.forma
    t_script(( 'Hello!'
    ));}\n", 0,
    false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  150. Haml::Helpers

    View Slide

  151. Haml.has_many Helpers
    % git grep "^ *def " lib/haml/helpers.rb
    lib/haml/helpers.rb: def initialize(method)
    lib/haml/helpers.rb: def to_s
    lib/haml/helpers.rb: def inspect
    lib/haml/helpers.rb: def self.action_view?
    lib/haml/helpers.rb: def init_haml_helpers
    lib/haml/helpers.rb: def non_haml
    lib/haml/helpers.rb: def find_and_preserve(input = nil, tags = haml_buffer.options[:preserve], &block)
    lib/haml/helpers.rb: def preserve(input = nil, &block)
    lib/haml/helpers.rb: def list_of(enum, opts={}, &block)
    lib/haml/helpers.rb: def html_attrs(lang = 'en-US')
    lib/haml/helpers.rb: def tab_up(i = 1)
    lib/haml/helpers.rb: def tab_down(i = 1)
    lib/haml/helpers.rb: def with_tabs(i)
    lib/haml/helpers.rb: def surround(front, back = front, &block)
    lib/haml/helpers.rb: def precede(str, &block)
    lib/haml/helpers.rb: def succeed(str, &block)
    lib/haml/helpers.rb: def capture_haml(*args, &block)
    lib/haml/helpers.rb: def haml_concat(text = "")
    lib/haml/helpers.rb: def haml_indent
    lib/haml/helpers.rb: def haml_tag(name, *rest, &block)
    lib/haml/helpers.rb: def haml_tag_if(condition, *tag)
    lib/haml/helpers.rb: def html_escape(text)
    lib/haml/helpers.rb: def escape_once(text)
    lib/haml/helpers.rb: def is_haml?
    lib/haml/helpers.rb: def block_is_haml?(block)
    lib/haml/helpers.rb: def merge_name_and_attributes(name, attributes_hash = {})
    lib/haml/helpers.rb: def with_haml_buffer(buffer)
    lib/haml/helpers.rb: def haml_buffer
    lib/haml/helpers.rb: def haml_bind_proc(&proc)
    lib/haml/helpers.rb: def prettify(text)
    lib/haml/helpers.rb: def is_haml?

    View Slide

  152. Haml.has_many Helpers

    Haml extends the Helpers module
    into each buffer instance

    Wow lovely, but who uses them?

    Again, let's just remove them.

    View Slide

  153. The code
    begin;extend Haml::Helpers;_hamlout =
    @haml_buffer =
    Haml::Buffer.new(haml_buffer);_erbout =
    _hamlout.buffer;@output_buffer =
    output_buffer ||=
    ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.format_sc
    ript(( 'Hello!'
    ));}\n", 0,
    false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  154. - Helpers
    begin;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer);_erbout =
    _hamlout.buffer;@output_buffer =
    output_buffer ||=
    ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.for
    mat_script(( 'Hello!'
    ));}\n", 0,
    false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  155. Buffers

    View Slide

  156. Haml.has_many buffers

    We can find 6 different buffer variables in the code

    _hamlout

    @haml_buffer

    haml_buffer

    _erbout

    @output_buffer

    output_buffer

    Why don't we just remove unneeded ones?

    View Slide

  157. The code
    begin;_hamlout = @haml_buffer =
    Haml::Buffer.new(haml_buffer);_erbout =
    _hamlout.buffer;@output_buffer =
    output_buffer ||=
    ActionView::OutputBuffer.new rescue
    nil;;_hamlout.push_text("#{_hamlout.for
    mat_script(( 'Hello!'
    ));}\n", 0,
    false);_erbout;ensure;@haml_buffer =
    @haml_buffer.upper if @haml_buffer;end;

    View Slide

  158. - buffers
    @output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new;@outpu
    t_buffer.concat("#{_hamlout.format_
    script(( 'Hello!'
    ));}\n", 0,
    false);@output_buffer.to_s

    View Slide

  159. html_safe

    View Slide

  160. Why not directly use
    SafeBuffer?

    Requiring the SafeBuffer means
    that the Haml gem is going to
    depend on Rails

    Let's do so! Who uses Haml off
    Rails?

    View Slide

  161. The code
    @output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new;@outpu
    t_buffer.concat("#{_hamlout.format_
    script(( 'Hello!'
    ));}\n", 0,
    false);@output_buffer.to_s

    View Slide

  162. + Rails
    @output_buffer = output_buffer ||=
    ActionView::OutputBuffer.new;@outpu
    t_buffer.concat('Hello!');@output_b
    uffer.to_s

    View Slide

  163. This is the goal

    I want to make Haml as fast as possible

    But these changes would bring many
    incompatibilities

    So I'm going to fork Haml and create my
    own version

    Which is named "Haml X"

    View Slide

  164. Haml X

    Haml X is still under construction

    Not yet published

    I'm working on it, and hopefully
    will be able to ship it soon

    View Slide

  165. Current status of Haml X
    Calculating -------------------------------------
    erb 69.021k i/100ms
    haml 6.263k i/100ms
    hamlx 9.026k i/100ms
    -------------------------------------------------
    erb 1.831M (± 3.2%) i/s - 9.180M
    haml 68.364k (± 5.0%) i/s - 344.465k
    hamlx 110.208k (± 4.3%) i/s - 550.586k

    View Slide

  166. Current status

    Still 16.6x slower than ERB

    But more than 50% faster than
    the original Haml

    View Slide

  167. Slim
    Part 3

    View Slide

  168. Slim

    Haml-like syntax

    "Automatic HTML escaping by
    default"

    "High performance"

    https://github.com/slim-template/slim

    View Slide

  169. IMO Slim is actually
    slimmer than current Haml

    Code is slim

    Built on Temple

    Renders through Tilt

    Less options

    No helper methods

    View Slide

  170. Temple

    A very cleverly written
    "Template compilation framework"

    Compiles the template into "the
    core abstraction"

    S-expression using Ruby Array

    View Slide

  171. Tilt

    "Generic interface to multiple
    Ruby template engines"

    https://github.com/rtomayko/tilt

    View Slide

  172. Slim

    Slim is awesome.

    Thank you slim, for being a good
    competitor :)

    View Slide

  173. Conclusion

    Now you understand how
    templates work

    Haml is so slow

    I'm going to improve it

    View Slide

  174. Conclusion

    Haml X is (hopefully) coming soon

    Stay tuned!

    View Slide

  175. end

    View Slide