Skip to content
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

Update slot content without rerendering rest of component #6351

Open
adamvleggett opened this issue Aug 11, 2017 · 33 comments
Open

Update slot content without rerendering rest of component #6351

adamvleggett opened this issue Aug 11, 2017 · 33 comments

Comments

@adamvleggett
Copy link

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.

@adamvleggett
Copy link
Author

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.

@LinusBorg
Copy link
Member

LinusBorg commented Aug 23, 2017

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.

@adamvleggett
Copy link
Author

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.

@LinusBorg
Copy link
Member

LinusBorg commented Aug 23, 2017

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.

@adamvleggett
Copy link
Author

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.

@adamvleggett
Copy link
Author

Also, calling the render function is typically much slower than performing a diff on two strings of data.

@rssfrncs

This comment has been minimized.

@LinusBorg

This comment has been minimized.

@JosephSilber
Copy link

JosephSilber commented Jun 12, 2018

@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.

@JosephSilber
Copy link

JosephSilber commented Jun 17, 2018

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:

  1. 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.

  2. 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 .vue file, 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.

@jacekkarczmarczyk
Copy link

jacekkarczmarczyk commented Jun 17, 2018

You can create a computed prop that returns an array of total order values for all orders

@JosephSilber
Copy link

@jacekkarczmarczyk the problem with that is that if any of the orders changes, all order totals then have to be recalculated.

@JosephSilber
Copy link

JosephSilber commented Sep 30, 2018

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:

  1. when slot content changes, only the child re-renders;
  2. 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

@steve-heine
Copy link

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.

@stygmate
Copy link

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.

@JosephSilber
Copy link

@stygmate the just-released v2.6.0-beta.2 includes #9371, which addresses this issue.

@jlsjonas
Copy link

jlsjonas commented Feb 25, 2019

@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 👍

@LinusBorg
Copy link
Member

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.

@jlsjonas
Copy link

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

@stygmate
Copy link

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

@LinusBorg
Copy link
Member

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.

@wparad
Copy link

wparad commented Apr 21, 2019

@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

@besnikh
Copy link

besnikh commented Apr 21, 2019

@LinusBorg I can show you a live project where we are using a lot of @change on a v-text-field

Vetura Kosove

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

@stygmate
Copy link

stygmate commented Apr 21, 2019

..., 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 !

@LinusBorg
Copy link
Member

LinusBorg commented Apr 21, 2019

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.

@wparad
Copy link

wparad commented Apr 21, 2019

@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?

@LinusBorg
Copy link
Member

LinusBorg commented Apr 21, 2019

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...

@adamvleggett
Copy link
Author

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.

@mesihtasci
Copy link

Is this fixed now in vue 3? @adamvleggett

@LinusBorg
Copy link
Member

LinusBorg commented Oct 28, 2020

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.

@renatodeleao
Copy link

renatodeleao commented May 20, 2021

@LinusBorg I think I have use-case similar to jlsjonas and wparad, but not to the OP example.

disclaimer

I'm posting it here because I think is related, if you think is not or is simply the way Vue works and I should read more about VDOM topic feel free to mark it as spam — no problem at all 🙏.

long read

Here's the demo sandbox

<template>
  <!-- parent -->
  <div class="parent">
    <slot v-bind="{ binding }"></div>
  </div>
</template>

<script>
export default {
  name: 'Parent',
  data() {
     return { binding: 0 }
  },
  // something that mutates 'binding', omitted for brevity, check demo
}
</script>
<template>
  <!-- child -->
  <div class="child">
    <slot>Default slot content</slot>
  </div>
</template>

<script>
export default {
  name: 'Child',
}
</script>

Usage

<parent v-slot:default="{ binding }">
  <!-- do stuff with binding -->
  <pre> binding: {{ binding }}.</pre>

  <!-- these do not do anything with "binding" but are re-rendered -->
  <child id="1" />
  <child id="2">Why am I updated?</child>
</parent>

Is it expected that the <Child>s are re-rendered even though nothing really changes for it? I was not expecting and makes me think about some of my renderless patterns. But maybe I missed some part in docs about it or missing something obvious?! 🤷

EDIT: actually updated <parent> and <child> to use render functions at the demo to properly check for re-render. It does make some sense if we see things through the render function eyes: the $scopedSlots.default({}) at <parent> needs to be called when "binding" updates thus re-rendering everything: I thought Vue diff mechanism could caught that and prevent <Child /> re-rendering. Even the provided binding is not reactive value, like a method, re-render is still triggered.

Real use case

  • <Parent> = is actually popper.js wrapper that I use to make flyout menus/tooltips/selectboxes wtv. And "binding" is for example the updated position object, that consumers can bind to any element or component to be dynamically positioned.

  • <Child> = can be anything, but a common example could be a Menu and a MenuItem that receives the text and other content — or other components ex: <app-icon> — via default slot content.

<v-popper #default="{ reference, popper, close }">
  <button v-bind="reference.attrs">Some Ref</button>
  <div :style="popper.styles">
     <!-- re-renders when popper.styles is updated -->
     <app-menu>
	     <app-menu-item>Go to x</app-menu-item>
	     <app-menu-item @click="close">Dismiss</app-menu-item>
	     <app-menu-item @click="close">
	        <app-icon name="pen" />
	        <span>Edit</span>
	     </app-menu-item>	        
	 </app-menu-item>
  </div>
<v-popper>

Even if I make wrapper components for the "slots" and orchestrate everything with provide/Inject, passing a reference to a method via slot scope also triggers a re-render. Not using slot-scope does not re-render as expected. The only alternative that I'm seeing is using $refs instead of slot scope, but I really don't like that idea.

Real-world-ish demo

Code example
<v-popper-provider>
  <v-popper-reference>Some Ref</v-popper-reference>
  <v-popper-el v-slot:default="{ close }">
     <!-- re-renders on VPopperEl internal style updates -->
     <app-menu>
	     <app-menu-item>El</app-menu-item>
	     <app-menu-item @click="close" >El</app-menu-item>
	 </app-menu-item>
  </v-popper-el>
<v-popper-provider>

The alternative is to use $refs and call method

<v-popper-provider ref="popper">
  <v-popper-reference>Some Ref</v-popper-reference>
  <v-popper-el >
     <!-- no scoped slot, no re-render, but refs feels like a dirty workaround -->
     <app-menu>
	     <app-menu-item>El</app-menu-item>
	     <app-menu-item @click="$refs.popper.close">El</app-menu-item>
	 </app-menu-item>
  </v-popper-el>
<v-popper-provider>

@jackysee
Copy link

Some articles introduce the use of 'renderless' component to provide e.g. data binding. I guess it's not recommended due to this issue? (if the slot is large)

@mxcabre
Copy link

mxcabre commented Sep 17, 2024

UP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests