You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The `ActionView::Helpers::TagHelper::TagBuilder` class renders HTML
elements based on the methods invoked on it. For example, `tag.input`
will render an `<input>` element, while `tag.turbo_frame` will render a
`<turbo-frame>` element.
The magic of the class is rooted in its definition of `#method_missing`.
The current implementation bakes-in special treatment of void HTML
elements (for example: `<input>`) and self-closing SVG elements (for
example: `<use />`). Despite its ahead-of-time knowledge of these
element names, calls to corresponding methods `tag.input` and `tag.use`
still rely on `#method_missing`.
This has performance implications.
This commit defines a new `TagBuilder.define_void_element` class method
to dynamically define methods for each known element. Then, the class
invokes the method for each element name in `HTML_VOID_ELEMENTS` and
`SVG_SELF_CLOSING_ELEMENTS`.
Calls to `.define_void_element` and `.define_self_closing_element` makr
`HTML_VOID_ELEMENTS` and `SVG_SELF_CLOSING_ELEMENTS` unnecessary, so
this commit removes those constant definitions.
Additionally, this call removes calls to
`TagHelper.ensure_valid_html5_tag_name` from the `TagBuilder` defined
element methods, since they're known ahead of time to be valid HTML.
Instead, only call `ensure_valid_html5_tag_name` from within the
`method_missing` calls, since those are determined at runtime and are
uncontrolled.
By cutting out the reliance on method missing, calls to `TagBuilder`
methods (like `tag.input`) become comparable to calls to the `tag` view
helper with positional arguments (like `tag(:input)`).
```
❯ ruby bench.rb
Warming up --------------------------------------
tag 73.438k i/100ms
tag_builder 79.910k i/100ms
Calculating -------------------------------------
tag 732.467k (± 0.9%) i/s - 3.672M in 5.013504s
tag_builder 810.981k (± 0.8%) i/s - 4.075M in 5.025632s
Comparison:
tag_builder: 810981.5 i/s
tag: 732467.4 i/s - 1.11x (± 0.00) slower
```
The results were from the following benchmark rendering void (`<input>`)
elements:
```ruby
# frozen_string_literal: true
require "bundler/setup"
require "action_view"
require "minitest/autorun"
require "benchmark/ips"
class Foo
include ActionView::Helpers
end
helpers = Foo.new
Benchmark.ips do |x|
x.report("tag") { helpers.tag("input", value: "foo") }
x.report("tag_builder") { helpers.tag.input(value: "foo") }
x.compare!
end
```
Similar to `tag.input` (a void HTML element), calls to `tag.div` are
become somewhat comparable to `tag("div")`:
```
❯ ruby bench.rb
Warming up --------------------------------------
content_tag 59.548k i/100ms
tag_builder 51.215k i/100ms
Calculating -------------------------------------
content_tag 595.067k (± 0.5%) i/s - 2.977M in 5.003570s
tag_builder 505.553k (± 2.2%) i/s - 2.561M in 5.067704s
Comparison:
content_tag: 595067.5 i/s
tag_builder: 505552.6 i/s - 1.18x (± 0.00) slower
```
The following benchmarks were collected to compare `tag("turbo-frame")`
(an unknown custom HTML element) and `tag.turbo_frame`:
```
❯ ruby bench.rb
Warming up --------------------------------------
content_tag 56.152k i/100ms
tag_builder 49.207k i/100ms
Calculating -------------------------------------
content_tag 561.134k (± 0.5%) i/s - 2.808M in 5.003567s
tag_builder 491.178k (± 0.3%) i/s - 2.460M in 5.009140s
Comparison:
content_tag: 561133.8 i/s
tag_builder: 491177.7 i/s - 1.14x (± 0.00) slower
```
0 commit comments