Skip to content

Commit 4ddedab

Browse files
authored
Update benchmark to be more representative, update docs on performance, remove object shapes optimization (#2329)
* update performance docs to reference more representative benchmark * Revert "Pre-allocate instance variables for better compatibility with Object Shapes (#2282)" This reverts commit afbac9b. * lints * more lints
1 parent 70cdb97 commit 4ddedab

19 files changed

+58
-210
lines changed

docs/CHANGELOG.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ nav_order: 6
3030

3131
*Stephen Nelson*
3232

33+
* Update documentation on performance to reflect more representative benchmark showing 2-3x speed increase over partials.
34+
35+
*Joel Hawksley*
36+
37+
* Add documentation note about instrumentation negatively affecting performance.
38+
39+
*Joel Hawksley*
40+
41+
* Revert object shapes optimization due to lack of evidence of improvement.
42+
43+
*Joel Hawksley*
44+
3345
## 4.0.0.alpha5
3446

3547
* BREAKING: `config.view_component_path` is now `config.generate.path`, as components have long since been able to exist in any directory.
@@ -40,10 +52,6 @@ nav_order: 6
4052

4153
*Joel Hawksley*
4254

43-
* Add internal optimization for Ruby object shapes.
44-
45-
*Adam Hess*, *Joel Hawksley*
46-
4755
## 4.0.0.alpha4
4856

4957
* BREAKING: Remove default initializer from `ViewComponent::Base`. Previously, `ViewComponent::Base` defined a catch-all initializer that allowed components without an initializer defined to be passed arbitrary arguments.

docs/guide/instrumentation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ ActiveSupport::Notifications.subscribe("render.view_component") do |event| # or
2626
end
2727
```
2828

29+
_Note: Enabling instrumentation negatively impacts the performance of ViewComponent._
30+
2931
## Viewing instrumentation sums in the browser developer tools
3032

3133
When using `render.view_component` with `config.server_timing = true` (default in development) in Rails 7, the browser developer tools display the sum total timing information in Network > Timing under the key `render.view_component`.

docs/index.md

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,26 +85,12 @@ ViewComponents use a standard Ruby initializer that clearly defines what's neede
8585

8686
### Performance
8787

88-
Based on several [benchmarks](https://github.com/viewcomponent/view_component/blob/main/performance/partial_benchmark.rb), ViewComponents are ~10x faster than partials in real-world use-cases.
88+
Based on several [benchmarks](https://github.com/viewcomponent/view_component/blob/main/performance/partial_benchmark.rb), ViewComponents are ~2.5x faster than partials:
8989

90-
The primary optimization is pre-compiling all ViewComponent templates at application boot, instead of at runtime like traditional Rails views.
91-
92-
For example, the `MessageComponent` template is compiled onto the Ruby object:
93-
94-
```ruby
95-
# app/components/message_component.rb
96-
class MessageComponent < ViewComponent::Base
97-
def initialize(name:)
98-
@name = name
99-
end
100-
101-
def call
102-
@output_buffer.safe_append = "<h1>Hello, ".freeze
103-
@output_buffer.append = (@name)
104-
@output_buffer.safe_append = "!</h1>".freeze
105-
@output_buffer.to_s
106-
end
107-
end
90+
```console
91+
Comparison:
92+
component: 6498.1 i/s
93+
partial: 2676.5 i/s - 2.50x slower
10894
```
10995

11096
### Code quality

lib/view_component/base.rb

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,6 @@ class Base
3434
class << self
3535
delegate(*ViewComponent::Config.defaults.keys, to: :config)
3636

37-
# Redefine `new` so we can pre-allocate instance variables to optimize
38-
# for Ruby object shapes.
39-
def new(...)
40-
instance = allocate
41-
instance.__vc_pre_allocate_instance_variables
42-
instance.send(:initialize, ...)
43-
instance
44-
end
45-
4637
# Returns the current config.
4738
#
4839
# @return [ActiveSupport::OrderedOptions]
@@ -56,29 +47,6 @@ def config
5647
end
5748
end
5849

59-
def __vc_pre_allocate_instance_variables
60-
@__vc_parent_render_level = 0
61-
@__vc_set_slots = {}
62-
@__vc_content_evaluated = false
63-
@current_template = nil
64-
@output_buffer = nil
65-
@lookup_context = nil
66-
@view_flow = nil
67-
@view_context = nil
68-
@virtual_path = nil
69-
@__vc_ancestor_calls = nil
70-
@__vc_controller = nil
71-
@__vc_content = :unset # some behaviors depend on checking for nil
72-
@__vc_content_set_by_with_content = nil
73-
@__vc_helpers = nil
74-
@__vc_inline_template = nil
75-
@__vc_inline_template_defined = nil
76-
@__vc_render_in_block = nil
77-
@__vc_request = nil
78-
@__vc_requested_details = nil
79-
@__vc_original_view_context = nil
80-
end
81-
8250
include ActionView::Helpers
8351
include ERB::Escape
8452
include ActiveSupport::CoreExt::ERBUtil
@@ -151,12 +119,14 @@ def render_in(view_context, &block)
151119
@__vc_requested_details ||= @lookup_context.vc_requested_details
152120

153121
# For caching, such as #cache_if
122+
@current_template = nil unless defined?(@current_template)
154123
old_current_template = @current_template
155124

156-
if block && __vc_content_set_by_with_content?
125+
if block && defined?(@__vc_content_set_by_with_content)
157126
raise DuplicateContentError.new(self.class.name)
158127
end
159128

129+
@__vc_content_evaluated = false
160130
@__vc_render_in_block = block
161131

162132
before_render
@@ -215,12 +185,16 @@ def render_parent
215185
#
216186
# When rendering the parent inside an .erb template, use `#render_parent` instead.
217187
def render_parent_to_string
218-
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
219-
@__vc_parent_render_level += 1
188+
@__vc_parent_render_level ||= 0 # ensure a good starting value
220189

221-
target_render.bind_call(self, @__vc_requested_details)
222-
ensure
223-
@__vc_parent_render_level -= 1
190+
begin
191+
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
192+
@__vc_parent_render_level += 1
193+
194+
target_render.bind_call(self, @__vc_requested_details)
195+
ensure
196+
@__vc_parent_render_level -= 1
197+
end
224198
end
225199

226200
# Optional content to be returned before the rendered template.
@@ -343,12 +317,12 @@ def __vc_request
343317
# @return [String]
344318
def content
345319
@__vc_content_evaluated = true
346-
return @__vc_content if @__vc_content != :unset
320+
return @__vc_content if defined?(@__vc_content)
347321

348322
@__vc_content =
349323
if __vc_render_in_block_provided?
350324
view_context.capture(self, &@__vc_render_in_block)
351-
elsif __vc_content_set_by_with_content?
325+
elsif __vc_content_set_by_with_content_defined?
352326
@__vc_content_set_by_with_content
353327
end
354328
end
@@ -357,23 +331,23 @@ def content
357331
#
358332
# @return [Boolean]
359333
def content?
360-
__vc_render_in_block_provided? || __vc_content_set_by_with_content?
334+
__vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
361335
end
362336

363337
private
364338

365339
attr_reader :view_context
366340

367341
def __vc_render_in_block_provided?
368-
@view_context && @__vc_render_in_block
342+
defined?(@view_context) && @view_context && @__vc_render_in_block
369343
end
370344

371-
def __vc_content_set_by_with_content?
372-
!@__vc_content_set_by_with_content.nil?
345+
def __vc_content_set_by_with_content_defined?
346+
defined?(@__vc_content_set_by_with_content)
373347
end
374348

375349
def content_evaluated?
376-
@__vc_content_evaluated
350+
defined?(@__vc_content_evaluated) && @__vc_content_evaluated
377351
end
378352

379353
def maybe_escape_html(text)
@@ -577,7 +551,7 @@ def render_template_for(requested_details)
577551
child.with_collection_parameter provided_collection_parameter
578552

579553
if instance_methods(false).include?(:render_template_for)
580-
vc_ancestor_calls = (!@__vc_ancestor_calls.nil?) ? @__vc_ancestor_calls.dup : []
554+
vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
581555

582556
vc_ancestor_calls.unshift(instance_method(:render_template_for))
583557
child.instance_variable_set(:@__vc_ancestor_calls, vc_ancestor_calls)

lib/view_component/inline_template.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module InlineTemplate
99
def method_missing(method, *args)
1010
return super if !method.end_with?("_template")
1111

12-
if @__vc_inline_template_defined
12+
if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
1313
raise MultipleInlineTemplatesError
1414
end
1515

@@ -38,11 +38,11 @@ def respond_to_missing?(method, include_all = false)
3838
end
3939

4040
def inline_template
41-
@__vc_inline_template
41+
@__vc_inline_template if defined?(@__vc_inline_template)
4242
end
4343

4444
def __vc_inline_template_language
45-
@__vc_inline_template_language
45+
@__vc_inline_template_language if defined?(@__vc_inline_template_language)
4646
end
4747

4848
def inherited(subclass)

lib/view_component/slot.rb

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,13 @@ class Slot
99
attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content
1010

1111
def initialize(parent)
12-
@content = nil
13-
@__vc_component_instance = nil
14-
@__vc_content = nil
15-
@__vc_content_block = nil
16-
@__vc_content_set_by_with_content = nil
1712
@parent = parent
1813
end
1914

2015
def content?
21-
return true if @__vc_content.present?
22-
return true if @__vc_content_set_by_with_content.present?
23-
return true if @__vc_content_block.present?
16+
return true if defined?(@__vc_content) && @__vc_content.present?
17+
return true if defined?(@__vc_content_set_by_with_content) && @__vc_content_set_by_with_content.present?
18+
return true if defined?(@__vc_content_block) && @__vc_content_block.present?
2419
return false if !__vc_component_instance?
2520

2621
@__vc_component_instance.content?
@@ -48,31 +43,31 @@ def with_content(args)
4843
# If there is no slot renderable, we evaluate the block passed to
4944
# the slot and return it.
5045
def to_s
51-
return @content if !@content.nil?
46+
return @content if defined?(@content)
5247

5348
view_context = @parent.send(:view_context)
5449

55-
if !@__vc_content_block.nil? && !@__vc_content_set_by_with_content.nil? && !@__vc_content_set_by_with_content.nil?
50+
if defined?(@__vc_content_block) && defined?(@__vc_content_set_by_with_content)
5651
raise DuplicateSlotContentError.new(self.class.name)
5752
end
5853

5954
@content =
6055
if __vc_component_instance?
6156
@__vc_component_instance.__vc_original_view_context = @parent.__vc_original_view_context
6257

63-
if !@__vc_content_block.nil?
58+
if defined?(@__vc_content_block)
6459
# render_in is faster than `parent.render`
6560
@__vc_component_instance.render_in(view_context) do |*args|
6661
@__vc_content_block.call(*args)
6762
end
6863
else
6964
@__vc_component_instance.render_in(view_context)
7065
end
71-
elsif !@__vc_content.nil?
66+
elsif defined?(@__vc_content)
7267
@__vc_content
73-
elsif !@__vc_content_block.nil?
68+
elsif defined?(@__vc_content_block)
7469
view_context.capture(&@__vc_content_block)
75-
elsif !@__vc_content_set_by_with_content.nil?
70+
elsif defined?(@__vc_content_set_by_with_content)
7671
@__vc_content_set_by_with_content
7772
end
7873

@@ -113,7 +108,7 @@ def respond_to_missing?(symbol, include_all = false)
113108
private
114109

115110
def __vc_component_instance?
116-
!@__vc_component_instance.nil?
111+
defined?(@__vc_component_instance)
117112
end
118113
end
119114
end

lib/view_component/slotable.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,8 @@ def set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
421421
end
422422
end
423423

424+
@__vc_set_slots ||= {}
425+
424426
if slot_definition[:collection]
425427
@__vc_set_slots[slot_name] ||= []
426428
@__vc_set_slots[slot_name].push(slot)
@@ -434,7 +436,7 @@ def set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
434436
def set_polymorphic_slot(slot_name, poly_type = nil, *args, **kwargs, &block)
435437
slot_definition = self.class.registered_slots[slot_name]
436438

437-
if !slot_definition[:collection] && @__vc_set_slots[slot_name]
439+
if !slot_definition[:collection] && defined?(@__vc_set_slots) && @__vc_set_slots[slot_name]
438440
raise ContentAlreadySetForPolymorphicSlotError.new(slot_name)
439441
end
440442

lib/view_component/slotable_default.rb

Lines changed: 0 additions & 18 deletions
This file was deleted.

performance/components/complex_component.html.erb

Lines changed: 0 additions & 5 deletions
This file was deleted.

performance/components/complex_component.rb

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)