Skip to content

Commit ceaffa4

Browse files
authored
Engine: Compile-time optimizations for Action View tag helpers (#1613)
This pull request adds compile-time optimization support for Action View tag helpers to `Herb::Engine`. When `optimize: true` is passed to `Herb::Engine`, the parser transforms Action View helper calls (`tag.*`, `content_tag`, `link_to`, `image_tag`, `javascript_tag`, `javascript_include_tag`, `turbo_frame_tag`) into their HTML equivalents at compile time, producing optimized Ruby output that avoids runtime helper dispatch. When compile-time optimizations are enabled, the engine checks whether the template contains supported Action View helpers via the `Herb::ActionView::HelperRegistry.supported` method. If helpers are detected, the parser is invoked with `action_view_helpers: true` and `transform_conditionals: true`, which also handles postfix if/unless and ternary conditionals containing helper calls. The pull request also includes comprehensive snapshot-based tests for all supported helpers, an `assert_optimized_output_match` helper that validates optimized output against both standard Herb and ActionView+Erubi to ensure the rendered output matches. Additionally, we added a YAML-driven benchmark framework in `bench/action_view/` with a `bin/bench` CLI for compile-time and render-time comparisons. Early benchmarks show render-time improvements of 3-22x depending on the template, with a realistic 192-line page layout rendering 22x faster, and a helper-heavy template with all static values reaching up to 150x faster. The trade-off is compile time: Herb's parser is currently 10-90x slower than Erubi at compile time, so optimization adds overhead that needs to be amortized over multiple renders. But this is typically won back with 15-100 renders depending on template complexity and could also be done ahead of time, instead of the current on-the-fly approach used in Action View today. > [!WARNING] > Compile-time optimizations are experimental. Output may differ from standard ActionView rendering. <img alt="CleanShot 2026-04-05 at 23 58 07@2x" src="https://github.com/user-attachments/assets/da407b38-e101-47e9-96a7-b58ed36fdf9d" /> Resolves #653 Enables #1111
1 parent 2a9d977 commit ceaffa4

263 files changed

Lines changed: 3645 additions & 244 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.rubocop.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Metrics/CyclomaticComplexity:
6262
- lib/herb/prism_inspect.rb
6363
- lib/herb/engine/**/*.rb
6464
- templates/template.rb
65+
- bench/**/*
6566

6667
Metrics/MethodLength:
6768
Max: 20
@@ -76,6 +77,7 @@ Metrics/MethodLength:
7677
- test/fork_helper.rb
7778
- test/snapshot_utils.rb
7879
- bin/**/*
80+
- bench/**/*
7981

8082
Metrics/AbcSize:
8183
Exclude:
@@ -93,6 +95,8 @@ Metrics/AbcSize:
9395
- test/snapshot_utils.rb
9496
- test/engine/rails_compatibility_test.rb
9597
- bin/**/*
98+
- bench/**/*
99+
- test/engine/action_view/action_view_test_helper.rb
96100

97101
Metrics/ClassLength:
98102
Exclude:
@@ -112,6 +116,7 @@ Metrics/ModuleLength:
112116
- test/**/*.rb
113117
- templates/**/*.rb
114118
- bin/lib/compare_helpers.rb
119+
- bench/**/*
115120

116121
Metrics/BlockLength:
117122
Max: 30
@@ -145,6 +150,7 @@ Metrics/PerceivedComplexity:
145150
- templates/template.rb
146151
- test/**/*.rb
147152
- bin/**/*
153+
- bench/**/*
148154

149155
Layout/LineLength:
150156
Enabled: false
@@ -177,6 +183,7 @@ Security/Eval:
177183
- bin/erubi-render
178184
- bin/compare-render
179185
- bin/compare-render
186+
- bench/action_view/benchmark_helper.rb
180187

181188
Security/MarshalLoad:
182189
Exclude:
@@ -192,3 +199,6 @@ Lint/UnusedMethodArgument:
192199

193200
Style/IfUnlessModifier:
194201
Enabled: false
202+
203+
Style/DocumentDynamicEvalDefinition:
204+
Enabled: false

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ gemspec
77
gem "prism", github: "ruby/prism", tag: "v1.9.0"
88

99
gem "actionview", "~> 8.0"
10+
gem "benchmark"
11+
gem "charm"
1012
gem "digest", "~> 3.2"
1113
gem "erubi"
1214
gem "irb", "~> 1.16"
@@ -17,9 +19,11 @@ gem "rake", "~> 13.2"
1719
gem "rake-compiler", "~> 1.3"
1820
gem "rake-compiler-dock", "~> 1.11"
1921
gem "rbs-inline", "~> 0.12"
22+
gem "reactionview", "~> 0.3.0"
2023
gem "reline", "~> 0.6"
2124
gem "rubocop", "~> 1.71"
2225
gem "sorbet"
26+
gem "turbo-rails", "~> 2.0"
2327

2428
# TODO: Remove once https://github.com/ruby/rbs/pull/2850 is merged and released
2529
gem "rbs", github: "marcoroth/rbs", branch: "psych-load-unsafe-file"

Gemfile.lock

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ PATH
4646
GEM
4747
remote: https://rubygems.org/
4848
specs:
49+
actionpack (8.1.2)
50+
actionview (= 8.1.2)
51+
activesupport (= 8.1.2)
52+
nokogiri (>= 1.8.5)
53+
rack (>= 2.2.4)
54+
rack-session (>= 1.0.1)
55+
rack-test (>= 0.6.3)
56+
rails-dom-testing (~> 2.2)
57+
rails-html-sanitizer (~> 1.6)
58+
useragent (~> 0.16)
4959
actionview (8.1.2)
5060
activesupport (= 8.1.2)
5161
builder (~> 3.1)
@@ -67,8 +77,40 @@ GEM
6777
uri (>= 0.13.1)
6878
ast (2.4.3)
6979
base64 (0.3.0)
80+
benchmark (0.5.0)
7081
bigdecimal (4.0.1)
82+
bubbles (0.1.1)
83+
bubbletea
84+
harmonica
85+
lipgloss
86+
bubbletea (0.1.4-aarch64-linux-gnu)
87+
lipgloss (~> 0.1)
88+
bubbletea (0.1.4-aarch64-linux-musl)
89+
lipgloss (~> 0.1)
90+
bubbletea (0.1.4-arm64-darwin)
91+
lipgloss (~> 0.1)
92+
bubbletea (0.1.4-x86_64-darwin)
93+
lipgloss (~> 0.1)
94+
bubbletea (0.1.4-x86_64-linux-gnu)
95+
lipgloss (~> 0.1)
96+
bubbletea (0.1.4-x86_64-linux-musl)
97+
lipgloss (~> 0.1)
98+
bubblezone (0.1.2-aarch64-linux-gnu)
99+
bubblezone (0.1.2-aarch64-linux-musl)
100+
bubblezone (0.1.2-arm64-darwin)
101+
bubblezone (0.1.2-x86_64-darwin)
102+
bubblezone (0.1.2-x86_64-linux-gnu)
103+
bubblezone (0.1.2-x86_64-linux-musl)
71104
builder (3.3.0)
105+
charm (0.1.0)
106+
bubbles
107+
bubbletea
108+
bubblezone
109+
glamour
110+
gum
111+
harmonica
112+
lipgloss
113+
ntcharts
72114
concurrent-ruby (1.3.6)
73115
connection_pool (3.0.2)
74116
crass (1.0.6)
@@ -95,22 +137,39 @@ GEM
95137
ffi (1.17.3-x86_64-linux-gnu)
96138
ffi (1.17.3-x86_64-linux-musl)
97139
fileutils (1.8.0)
140+
glamour (0.2.2-aarch64-linux-gnu)
141+
glamour (0.2.2-aarch64-linux-musl)
142+
glamour (0.2.2-arm64-darwin)
143+
glamour (0.2.2-x86_64-darwin)
144+
glamour (0.2.2-x86_64-linux-gnu)
145+
glamour (0.2.2-x86_64-linux-musl)
146+
gum (0.3.2-aarch64-linux)
147+
gum (0.3.2-arm64-darwin)
148+
gum (0.3.2-x86_64-darwin)
149+
gum (0.3.2-x86_64-linux)
150+
harmonica (0.1.1)
98151
i18n (1.14.8)
99152
concurrent-ruby (~> 1.0)
100153
io-console (0.8.2)
101154
irb (1.16.0)
102155
pp (>= 0.6.0)
103156
rdoc (>= 4.0.0)
104157
reline (>= 0.4.2)
105-
json (2.18.0)
158+
json (2.19.2)
106159
language_server-protocol (3.17.0.5)
107160
lint_roller (1.1.0)
161+
lipgloss (0.2.2-aarch64-linux-gnu)
162+
lipgloss (0.2.2-aarch64-linux-musl)
163+
lipgloss (0.2.2-arm64-darwin)
164+
lipgloss (0.2.2-x86_64-darwin)
165+
lipgloss (0.2.2-x86_64-linux-gnu)
166+
lipgloss (0.2.2-x86_64-linux-musl)
108167
listen (3.10.0)
109168
logger
110169
rb-fsevent (~> 0.10, >= 0.10.3)
111170
rb-inotify (~> 0.9, >= 0.9.10)
112171
logger (1.7.0)
113-
loofah (2.25.0)
172+
loofah (2.25.1)
114173
crass (~> 1.0.2)
115174
nokogiri (>= 1.12.0)
116175
lz_string (0.3.0)
@@ -120,18 +179,24 @@ GEM
120179
minitest-difftastic (0.2.1)
121180
difftastic (~> 0.6)
122181
mutex_m (0.3.0)
123-
nokogiri (1.19.0-aarch64-linux-gnu)
182+
nokogiri (1.19.2-aarch64-linux-gnu)
124183
racc (~> 1.4)
125-
nokogiri (1.19.0-aarch64-linux-musl)
184+
nokogiri (1.19.2-aarch64-linux-musl)
126185
racc (~> 1.4)
127-
nokogiri (1.19.0-arm64-darwin)
186+
nokogiri (1.19.2-arm64-darwin)
128187
racc (~> 1.4)
129-
nokogiri (1.19.0-x86_64-darwin)
188+
nokogiri (1.19.2-x86_64-darwin)
130189
racc (~> 1.4)
131-
nokogiri (1.19.0-x86_64-linux-gnu)
190+
nokogiri (1.19.2-x86_64-linux-gnu)
132191
racc (~> 1.4)
133-
nokogiri (1.19.0-x86_64-linux-musl)
192+
nokogiri (1.19.2-x86_64-linux-musl)
134193
racc (~> 1.4)
194+
ntcharts (0.1.2-aarch64-linux-gnu)
195+
ntcharts (0.1.2-aarch64-linux-musl)
196+
ntcharts (0.1.2-arm64-darwin)
197+
ntcharts (0.1.2-x86_64-darwin)
198+
ntcharts (0.1.2-x86_64-linux-gnu)
199+
ntcharts (0.1.2-x86_64-linux-musl)
135200
parallel (1.27.0)
136201
parser (3.3.10.1)
137202
ast (~> 2.4.1)
@@ -145,13 +210,30 @@ GEM
145210
date
146211
stringio
147212
racc (1.8.1)
213+
rack (3.2.5)
214+
rack-session (2.1.1)
215+
base64 (>= 0.1.0)
216+
rack (>= 3.0.0)
217+
rack-test (2.2.0)
218+
rack (>= 1.3)
219+
rackup (2.3.1)
220+
rack (>= 3)
148221
rails-dom-testing (2.3.0)
149222
activesupport (>= 5.0.0)
150223
minitest
151224
nokogiri (>= 1.6)
152-
rails-html-sanitizer (1.6.2)
153-
loofah (~> 2.21)
225+
rails-html-sanitizer (1.7.0)
226+
loofah (~> 2.25)
154227
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
228+
railties (8.1.2)
229+
actionpack (= 8.1.2)
230+
activesupport (= 8.1.2)
231+
irb (~> 1.13)
232+
rackup (>= 1.0.0)
233+
rake (>= 12.2)
234+
thor (~> 1.0, >= 1.2.2)
235+
tsort (>= 0.2)
236+
zeitwerk (~> 2.6)
155237
rainbow (3.1.1)
156238
rake (13.3.1)
157239
rake-compiler (1.3.1)
@@ -167,6 +249,9 @@ GEM
167249
erb
168250
psych (>= 4.0.0)
169251
tsort
252+
reactionview (0.3.0)
253+
actionview (>= 7.0)
254+
herb (>= 0.9.0, < 1.0.0)
170255
regexp_parser (2.11.3)
171256
reline (0.6.3)
172257
io-console (~> 0.5)
@@ -195,13 +280,19 @@ GEM
195280
strscan (3.1.7)
196281
terminal-table (4.0.0)
197282
unicode-display_width (>= 1.1.1, < 4)
283+
thor (1.5.0)
198284
tsort (0.2.0)
285+
turbo-rails (2.0.23)
286+
actionpack (>= 7.1.0)
287+
railties (>= 7.1.0)
199288
tzinfo (2.0.6)
200289
concurrent-ruby (~> 1.0)
201290
unicode-display_width (3.2.0)
202291
unicode-emoji (~> 4.1)
203292
unicode-emoji (4.2.0)
204293
uri (1.1.1)
294+
useragent (0.16.11)
295+
zeitwerk (2.7.5)
205296

206297
PLATFORMS
207298
aarch64-linux
@@ -216,6 +307,8 @@ PLATFORMS
216307

217308
DEPENDENCIES
218309
actionview (~> 8.0)
310+
benchmark
311+
charm
219312
digest (~> 3.2)
220313
erubi
221314
herb!
@@ -229,10 +322,12 @@ DEPENDENCIES
229322
rake-compiler-dock (~> 1.11)
230323
rbs!
231324
rbs-inline (~> 0.12)
325+
reactionview (~> 0.3.0)
232326
reline (~> 0.6)
233327
rubocop (~> 1.71)
234328
sorbet
235329
steep!
330+
turbo-rails (~> 2.0)
236331

237332
BUNDLED WITH
238333
2.7.2

0 commit comments

Comments
 (0)