-
Notifications
You must be signed in to change notification settings - Fork 444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use single @output_buffer when rendering #974
Conversation
This change isn't great performance wise. I'm hoping to find some time to try optimizing it.
|
Not requiring HAML/Slim seemed to increase the benchmark a bit. view_component ❯ be rake benchmark
* development - set it to false
* test - set it to false (unless you use a tool that preloads your test environment)
* production - set it to true
Warming up --------------------------------------
component: 2.491k i/100ms
partial: 890.000 i/100ms
Calculating -------------------------------------
component: 25.042k (± 2.4%) i/s - 251.591k in 10.052708s
partial: 9.043k (± 2.5%) i/s - 90.780k in 10.045240s
Comparison:
component:: 25042.5 i/s
partial:: 9043.0 i/s - 2.77x (± 0.00) slower |
Looks like the change I made to the benchmark actually decreased the numbers too. Here's
|
Even more benchmarks: Main
This branch
|
11e2358
to
42f45c0
Compare
@BlakeWilliams Is there any news on this? |
@23tux Hi! There is. This mostly works, but there is an edge case I'm seeing in production applications that I haven't had time to track down. It would be super helpful to have a failing test case for that scenario or other scenarios production apps might run into. If anyone would like to try to run this locally and write a failing test case, that would help me move forward with this. |
I've the same or similar issue with I'm busy this week, but I'll try to found time to write some failing test case. I've tested your branch on our project and solve the issue btw 👍 |
I recently ran into the same problem, or my assumption is that it is the same root problem anyway. I was trying to call some vanilla Rails helpers from my view component with similar results. My pseudo-code looked like this: <%= helpers.my_rails_helper do %>
<h1>Content inside ViewComponent</h1>
<% end %> A simplified version of the def my_rails_helper
contents = capture(&block)
# run conditional logic based on captured contents
# ...
# main output
content_tag(:div, contents, id: "my-id", ...)
end The net result was that the <h1>Content inside ViewComponent</h1>
<div id="my-id"></div> I tried various approaches to try and fix this, and was able to get the code below to work. It's entirely possible that this is a bad idea, but I wanted to share in case it provided additional insight into the matter. I added these methods to the def respond_to_missing?(method, include_all)
helpers.respond_to?(method, include_all) || super
end
def method_missing(method, *args, &block)
if helpers.respond_to?(method)
helpers.method(method).unbind.bind(self).call(*args, &block)
else
super
end
end |
end | ||
|
||
def test_renders_form_with_labels_with_block_correctly | ||
render_inline(FormForComponent.new) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be FormWithComponent
here?
I have a failing test case that I think could be caused by the issue you're trying to fix here, added as the commit mhw@58dacd7 I'm trying to bring some partials across to ViewComponent, and this test replicates an issue I ran into with a partial that uses
while I think it should be
That's assuming I've not made a mistake in the use of ViewComponent. |
I've been doing a bit more digging on this one and have a branch (here) that takes the work here and addresses a couple of additional issues to give something that's working for me in an app where I'm trying to convert a bunch of nested partials in to ViewComponents. I think the basic approach here is sound: the Rails The one issue I have run into is with Haml: Haml 5 also patches |
@BlakeWilliams can you rebase this? What do you think about releasing this change behind a config setting, marked as experimental, so it can be tested easily on real-world apps? My current use case is rendering view components in other view components, within a form managed by a custom FormBuilder (that's what we are experimenting with over at view_component-form). With the latest |
@Spone I can write a TODO to rebase it, but as-is it's not working in a production application I've tried it on. There's something about the new behavior that breaks existing behavior in apps. Unfortunately I don't have a ton of time to dedicate to the issue so it's difficult to consistently work on the problem since it requires a good amount of context to properly understand. |
If you can elaborate a bit on this, when you have time. I can dedicate some time to try and replicate the issue in our production apps, if I know what to look for. |
@Spone You might want to look at the branch I'm working on: I've rebased it a couple of times so it is not too far behind main. I also discovered a further problem with the implementation in this PR, which might be the same issue @BlakeWilliams mentions. The code in
Broadly I think the issue is that The approach I took to addressing this is to redefine In the app where I'm introducing View Component I am planning to continue converting a set of partials in to components to give me more confidence that the solution I've got is actually correct. I've tried it on #1227 because I've also been meaning to spend a bit more time on the solution to see if there's a more efficient approach, but I've been working on other things recently. |
e430427
to
02bb7af
Compare
I just pushed up the results on what I've been hacking on since Sunday. I originally took a different approach, trying to use a proxy object that manages a buffer and is shared across I took another try at the subscriber model this morning and ran into the issue I mentioned earlier, but thanks to @mhw (seriously, great find!) pointing out that See https://github.com/github/view_component/pull/974/files#diff-83ab3a050e037c39b32215700aff22c51283cf9e35710d0335c2bae28b5a81aeR35-R45 for what fixed the issue. I think that could make a great upstream change for Rails, and any other place we explicitly set HAML is still broken, but that's somewhat expected. I can try to hack on this a bit more to see if I can gate it behind a flag and run some benchmarks to see if this is great/bad. I'm really curious about how this could be upstreamed for Rails as some kind of generic buffer manager so we can remove the need for patches or a global buffer pub/sub object and if they'd accept a patch. |
02bb7af
to
25bfef2
Compare
25bfef2
to
f637ba9
Compare
Performance took a hit because of all the extra
|
Just wanted to confirm that your revised branch works great in the app I'm working on where I'd run in to issues previously. It's pretty simple, but I'm at the stage of switching partials to view components and hence was running in to these buffer handling issues. I'd thought about what change in Rails might make this easier to do, and the simplest thing I'd come up with was to extend
That would be a breaking change though, at least with respect to any other gems that work by updating The other thought I'd had was whether anything could be inferred from the binding of the block being yielded to. Passing a block from one view context to be evaluated in a different view context seems to be the issue, as the block being passed will still be outputting to the original view context's buffer. I wondered if looking at |
Thanks for all the work on this! I have just tried this branch and solves the problem for me 🎉 . In terms of performance in my unscientific tests, Im seeing ~26-28% slower rendering times compared to
|
Thanks for sharing that! I think that's an expected result of this change, at least as-is. In order to have consistent rendering within the Rails |
I'm wondering if there could be a way to minimize the overall performance penalty by only applying this behavior (capturing the component render) when it is needed. Maybe some kind of flag to declare manually on the component class, when the component needs to reuse the existing In my current app, most components are currently working fine and don't need this, so I could apply the new behavior only to components interacting with forms. |
Hey everyone! Over the last couple of days I've been hacking on @BlakeWilliams' original work in an attempt to implement the single output buffer concept using the proxy model he mentioned instead of the subscriber model. So far, things look very promising. I've been able to get the performance hit down to about 18%, which should be almost entirely mitigated by this companion performance PR. I as able to verify the changes in a large production application (github.com) and everything seems to be working. I would be grateful however if others could give it a spin! Please let me know if you run into anything weird :) /cc @Spone @stevegeek @mhw @percyhanna @nicolas-brousse @23tux |
replaced with #1650, which is likely getting merged soon. |
When rendering components we sometimes run into incompatibilities with
Rails due to Rails expecting a single
@output_buffer
when renderingtemplates. This isn't true when using view components due to each
component having its own
@output_buffer
.The most obvious issue I've run into due us not having a single
@output_buffer
can be recreated by repeating the following:form_for
f
variable to a componentf.label
while passing it a blockThis will cause the block content to be rendered incorrectly, usually
above the resulting
<label>
tag.The tl;dr of that behavior is that
form_for
captures theview_context
of the template it's called in. When trying to evaluatethe block passed to
f.label
it callsview_context.capture
but theblock was defined and is run in the context of the component, not
view_context
! This causes the block content to be rendered in thecomponent's
@output_buffer
(since it's not capturing).This attempts to resolve the issue by tracking and using a single
@output_buffer
. When a component is rendered we find the top-levelActionView::Base
instance in the render hierarchy and use its@output_buffer
. We then tell the top-level renderer that we are one ofits children.
Now, when any
ActionView::Base
instance in the rendering hierarchychanges its
@output_buffer
, it propagates that change to allActionView::Base
instances. This matches Rails' expectations andresults in the previously broken behavior working as-expected.