vue
vue copied to clipboard
Update slot content without rerendering rest of component
What problem does this feature solve?
I have developed some components that can generate large amounts of HTML, and allow content to be added via slot. It appears that if the slot content is updated, the render function is called for the component; however, this seems like something that could be avoided through optimization which would significantly improve the performance of my component in some instances.
Is this possible?
What does the proposed API look like?
Not proposing API changes.
Note: I understand this wouldn't be possible for scoped slots. However, an optimization that might help even for scoped slots would be to only trigger rerender of the child component if the rerender of the parent component generated a delta for the actual slot content.
Well, I think it would be possible to implement something that would diff slot contents before updateiung the components but - ignoring difficulties in implementing this for the moment, because I can't say much about this right now - it would come with a performance tradeoff:
With your proposal, we would save rendering the virtualdom of the child if nothing in the slot changed - but everytime the diffing of slot content does find changes in the slot content, we would be diffing the slot content twice - because after the render function of the child has run, the new vdom has to be diffed again.
Essentially this means that now, children with big templates and small slot contents would run better, while children with small templates and big slots woudl run worse when changes happen.
Not sure what is better...
Also, technically the child would keep an outdated virtualDOM, because while the content of the slot nodes is the same, the parent created fresh nodes when it re-rendered, so I suspect that this might be a technical hurdle.
In my case it's a slot that the component uses in a v-for loop to apply to hundreds or more repeats. In this case it's unquestionably faster to calculate the delta. I wonder if this can be reasonably detected.
I wonder if this can be reasonably detected.
Hardly, and if so, only during compile time, not runtime. That would require some analysis during compilation that would have to derminate when a template is "expensive, then set some flag so the component resolts to slot diffing during runtime, and doesn't for cheap components.
Sounds easy but measuring "expensiveness" would be very tricky considering v-if having a big impactr on when a temnplate is actually expensive and when not, and the small fact that we cannot statically analyse during compilation how big the arrays that you render will be during runtime, etc. pp.
A new API would be thinkable to set that flag manually, at least in theory.
I would say if either:
- the slot is used inside any loop
- the slot contents are smaller than the template
Then that's a time it's probably worth it to test the delta.
In my particular case, it would save me having to make some rather unintuitive changes to the structure of a library that's used by a lot of developers in my company.
Also, calling the render function is typically much slower than performing a diff on two strings of data.
Hi.
Is this issue still active?
Yes.
@LinusBorg
everytime the diffing of slot content does find changes in the slot content, we would be diffing the slot content twice - because after the render function of the child has run, the new vdom has to be diffed again.
Would it be possible to introduce a new watcher boundary API? This would be an internal implementation, to which template slot content would compile to.
This way, the slot's content wouldn't even have to re-render if the data it depends on doesn't change. Since it doesn't re-render, it doesn't need to be diffed.
Just to show another use-case of how I bumped into this:
Vue's computed properties currently can't take any arguments. Instead, if you want to use some calculated data in a loop (which is generally where you'd need to pass data into a computed), you have to resort to one of these:
-
Using a regular method, such as
calculateOrderTotal(order).However, using a regular method means that the data is recomputed with every render. That's bad for performance.
-
Create a separate functional component for the list item, and create an instance of that component for each item in the list, such as
<Order v-for="order of orders" :order="order">. The component will then only re-render when the data passed to it is changed, which is what we want.However, putting the template in the component is not ideal because a) these are usually small snippets that don't really warrant being in their own template b) if the component uses a template then it needs its own
.vuefile, which makes this solution feel even heavier than it already is.So the solution I came up with is to have the component only calculate the data, but render all of its HTML via the default slot, thereby forgoing the template altogether. And now we get to the crux of the issue: if we pass content in the slot to the child component, it will always re-render along with the parent, which means that the data will also be recalculated on every single render1. This totally negates any benefit we got over using a regular method.
Here's demo in action: https://codepen.io/JosephSilber/pen/MXJZro
1 A potential solution would be to have a computed property within the child component, but then the component can no longer be functional. When rendering a big list, using functional components makes a huge difference.
You can create a computed prop that returns an array of total order values for all orders
@jacekkarczmarczyk the problem with that is that if any of the orders changes, all order totals then have to be recalculated.
Good news! It seems like this will be resolved in Vue 3.0:
All compiler-generated slots are now functions and invoked during the child component’s render call. This ensures dependencies in slots are collected as dependencies for the child instead of the parent.
This means that:
- when slot content changes, only the child re-renders;
- when the parent re-renders, the child does not have to if its slot content did not change.
https://medium.com/the-vue-point/plans-for-the-next-iteration-of-vue-js-777ffea6fabf
will there be a 2.6 update to fix this?
I have a basic spreadsheet like app where some slots are overridden with slots representing validation or special formatting of the data. When a user updates a the model within an input and tabs to the next input, the components child slots re-render causing the parent to re-render, and the user's input box loses its focus.
Any news on this ? it make some code/lib (using a lot slots) unusable. In my case it make Vuetify very performance hungry as a lot of thing rerender without need.
@stygmate the just-released v2.6.0-beta.2 includes #9371, which addresses this issue.
@JosephSilber That doesn't seem to address dynamically created slots, right?
We have a lot of forms where the structures are defined by a json document on load, this does mean that the slots are dynamically defined (even if they don't change once loaded in)
A big issue the rerender is causing is that certain sub-components fetch extra data, which it's doing on every re-render in this case. If anyone can provide a viable workaround that would be great too 👍
We have a lot of forms where the structures are defined by a json document on load, this does mean that the slots are dynamically defined (even if they don't change once loaded in)
This short explanation doesn't really explain what exactly you do and mean by "dynamically created slots".
I would advise you to join us in the forums @ forum.vuejs.org and open a more in-depth topic explaining your situation there.
A .json fetched from the server (see docs for format example)
all "custom": true, fields are translated to a slot in EnsoForm.vue which are then filled by the page implementing the form.
A simple example is a custom multi-select where a sum is added
<template slot="example" slot-scope="{ field, errors, i18n, locale }">
<div>
<select-field
:errors="errors"
:field="field"
:i18n="i18n"
:locale="locale"
ref="example"
:custom-params="{ repairType: lastParent.id || null }"
@input="serviceSelected"
/>
<span>Total: {{ total }}</span>
</div>
</template>
Currently, when the total is updated the whole form is re-rendered, which causes all (server-side) <select-field> components to fetch their options from the server again
any news for this one ? Vuetify (the most stared Vuejs project) seems generate a lot of slot dynamically and performance are really really bad in some case. vuetifyjs/vuetify#6201
Would be helpful to have some. Up-to-date example that demonstrates the effect, especially with the new slot syntax.
From what I've read I don't entirely get the problem.
@LinusBorg, it is really quite simple if you have an component with a slot
<component>
<slot :value="value" @change="v => value = v" />
</component>
Whenever the @change handler is fired value is updated. That is good. The problem is that when this value is updated vue rerenders the slot. That is exactly what you would want to happen and it does. The problem is that it does this by re-rendering the whole component not just the slot. You can test this out by putting anything else in the component and it get re-rendered too.
<component>
<input />
<slot :value="value" @change="v => value = v" />
</component>
Set the focus to the input field and when value changes, you'll the focus leave the input
@LinusBorg I can show you a live project where we are using a lot of @change on a v-text-field
PS. The language is Albanian but if you write in a good pc you will not notice a delay, but trying to write in input from a mobile (especially Android) will make typing a hell.
Cheers
..., but trying to write in input from a mobile (especially Android) will make typing a hell.
@LinusBorg @besnikh exactly the same problem in my app !
I see what you mean but I don't really see a via able way to accomplish this except not using a virtualDOM, which means writing a new framework, essentially.
You find similar challenges in all vDom based frameworks (react etc.) - to update a slot he whole patent has to re-render in order to determine what to even send to the slot, that's determined by the render function.
And nested slots that do a lot wig work on re-renders get expensive if the dependency that's being updated by e.g. an input is being provided by some distant ancestor-component.
For the framework that we have, we should rather investigate better patterns to compose our components in order to prevent these deep re-renders.
@LinusBorg I'm not an expert in virtual DOMs, so I'm interested in doing a 5 whys here:
to update a slot he whole patent has to re-render in order to determine what to even send to the slot, that's determined by the render function.
Why? What's the importance of it being a slot? If it wasn't a slot it would work correctly, and the resultant html and javascript look the same. The parent would get the updated property intentionally and then the child would be afterwards. However, we know something, we know that this isn't a property changing, instead we know it is a slot changing. Soooo....
I'm going to say something that is probably dumb:
- Why don't we just wrap the slot in a component and treat it as a component that has inputs passed in: i.e.
<component :props="parent">
<slot-component :props="parent + child">
<input>
</slot-component>
<component>
but instead of those inputs parent here being edit which cause the component to rerender, bypass re-rendering the component and just pass those same props to the slot-component. Since we know it is a slot what could possibly make the component need to re-render?
I'll answer aboutbrh why tomorrow, it's 1am here.
But as I see a risk of us talking past each other I would still be thankful for an actually runnable example clearly demonstrating your issue instead of 3 lines from above.
The thing about the focus is clearly not the performance issue we are discussing here...
Solving this seems to require decoupling the scope of rendering from the scope of a component. This will be coming with Vue 3, I think it is highly unlikely in Vue 2.
Is this fixed now in vue 3? @adamvleggett
Aside from the optimizations we already introduced to Vue 2 with v-slot: No.
VirtualDOM implementations rely on this behavior in general. Vue 3 is no different.
Vue 3 might offer improvements in terms of performance as we now only need to re-render and diff vDOM for dynamic elements - if you generate large structured of static HTML, those can now be ignored by the renderer due to compiler optimizations.