Share
Embed
Template Engines in Ruby
Slides for RubyConf 2014 talk "Template Engines in Ruby" http://rubyconf.org/program#prop_812
Akira Matsuda
November 19, 2014
Transcript
175 slides
-
http://rubykaigi.org/2014 The slides are available from the "SCHEDULE" link The
videos are available from the "SCHEDULE" link Check 'em out! -
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 -
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 :< -
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) -
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 -
This includes documentation, comments % grep "^ *#" erb.rb |
wc -l #=> 517 So, the code is actually less than 500 lines -
Embedding Ruby code puts ERB.new('1 + 1 = <%= 1
+ 1 %>').result #=> "1 + 1 = 2" -
Embedding Ruby code puts ERB.new('<% if true %>foo<% else %>bar<%
end %>').result #=> "foo" -
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 -
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 -
ERB::Compiler#compile Prepares a buffer Scans through the template, splits the
template into tokens, then executes a command for each token -
add_put_cmd Pushes a Ruby code String into the buffer @put_cmd
=> method content_dump(content) => params -
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 -
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") -
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) -
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 -
ActionView::Template#co mpile! module ActionView class Template def compile!(view) return if
@compiled ... mod = view.singleton_class ... compile(mod) ... @compiled = true ... ennnd -
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 -
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 -
What ActionView render does ActionView defines a Ruby method on
the view object The method body is generated by ERB.new(template).src -
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 -
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 -
ActionView::Template::Handlers::ERB.erb _implementation module ActionView class Template module Handlers class ERB
... class_attribute :erb_implementation self.erb_implementation = Erubis ... ennnnd -
Erubis Yet another Pure Ruby implementation of eRuby A ruby
gem Created by Makoto Kuwata (@makotokuwata) https://github.com/kwatch/erubis -
According to its README "Very fast, almost three times faster
than ERB" "Auto escaping support" "Ruby on Rails support" ... and so on -
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 -
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 -
The ActionView tests do not pass! :bomb: % bundle ex
rake 1530 runs, 3434 assertions, 17 failures, 29 errors, 1 skips -
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... -
This causes a syntax error output_buffer = render_erb( "<%= form_tag('http://
www.example.com') do %>Hello world! <% end %>") -
Because this is not allowed in eRuby spec <%= form_for
@article do |f| %> ... <% end %> -
Do you remember that it had to be like this
in Rails 1.x and 2.x? <% form_for @article do |f| %> ... <% end %> -
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 -
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 %> -
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 -
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 -
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 -
I noticed this 2 years ago, And asked Seki-san if
we could extend ERB to be "Rails compatible" before Ruby 2.0 release -
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(-) -
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. -
So what is BLOCK_EXPR? And why is this problematic? Let's
carefully take a look at the Regexp. -
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 -
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. -
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'){" -
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! -
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 -
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 -
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! -
If I change the BLOCK_EXPR to /\s*(do|\{)(\s*\|[^|]*\|)?\s*\Z/ <%= @todo %>
Now ActionView regards this code as a block call! -
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 -
The Ruby parser is a nightmare. (@nobu) Let us keep
his words in mind. -
That is why this syntax is not acceptable in ERB
<%= form_for @article do |f| %> ... <% end %> -
Anyway, "ERB or Erubis" is not a big deal We
don't use neither of them after all :trollface: -
What is Haml? A "templating haiku" "well-indented" Implemented in Ruby
Created in May 2006 http://haml.info/ -
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; -
ERB ERB.new("<% 'Hello!' %>").src #=> "#coding:UTF-8\n_erbout = ''; 'Hello!' ;
_erbout.force_encoding(__ENCODING__)" -
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; -
But seriously, We don't usually have to care what is
executed behind But isn't this too much? It apparently looks slow... -
I said, compilation overhead doesn't matter But the generated Ruby
code DOES matter Yes, it's time to benchmark -
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 -
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 -
Result Greeting "Hello!" in Haml is 26x slower than ERB
But this is just a micro benchmark -
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!" -
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 -
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 -
Result Haml is still 2x slower than ERB In a
semi-micro banchmark -
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 -
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 -
Result On the real "Hello!" app, an ERB template renders
20ʙ% faster than a Haml one -
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; -
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/ -
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;; -
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;; -
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. -
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; -
- 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; -
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? -
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. -
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; -
- 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; -
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? -
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; -
- buffers @output_buffer = output_buffer ||= ActionView::OutputBuffer.new;@outpu t_buffer.concat("#{_hamlout.format_ script(( 'Hello!'
));}\n", 0, false);@output_buffer.to_s -
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? -
The code @output_buffer = output_buffer ||= ActionView::OutputBuffer.new;@outpu t_buffer.concat("#{_hamlout.format_ script(( 'Hello!'
));}\n", 0, false);@output_buffer.to_s -
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" -
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 -
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 -
Current status Still 16.6x slower than ERB But more than
50% faster than the original Haml -
Slim Haml-like syntax "Automatic HTML escaping by default" "High performance"
https://github.com/slim-template/slim -
IMO Slim is actually slimmer than current Haml Code is
slim Built on Temple Renders through Tilt Less options No helper methods -
Temple A very cleverly written "Template compilation framework" Compiles the
template into "the core abstraction" S-expression using Ruby Array -
Slim Slim is awesome. Thank you slim, for being a
good competitor :) -
Conclusion Now you understand how templates work Haml is so
slow I'm going to improve it