Vue.js - Takes too long to 'destroy' components - javascript

1I have a time-table component that is created with vue.js and it includes around 200 child timeline components as nested form (I wanted to upload image but couldn't without 10 reputations).
The problem now is that it takes more than 6 seconds to destroy this component.
Chrome says that 'remove' function (,which is called by vue.js everytime we destroy a component,) is called many times, and each of them takes around 20 - 40ms.
The vue.js remove function is like below:
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
and it seems that the first argument ,arr, is either a few VueComponents or more than 2000 Watcher objects.
Now, my questions are:
1. What is 'Watcher' in this context and why the number it exceeds 2000?
2. Why it takes such long time despite I do not handle like 10000 components or so?
I guess it is the matter of specification of vue.js, but please help me if you have a similar problem or have any idea about this matter. Thank you!
Above is how the timeline component appears, and each of gray-background panels and purple background panel(with a man icon) are child components.
When you click a purple panel, vue-router makes routing to the page of the detail, and at that time all of components are destroyed (that is when the problems above occurs)

We have experienced similar issues and found that all share the same underlying problem: too many components that depend on the same reactive object. These are the 3 main cases that may impact any project:
Many router-link components
Many components (any kind) when Vue I18n is installed
Many components that directly access the Vuex store on its render or computed properties.
Our approach is to avoid accessing shared reactive objects on the render and computed properties functions. Instead, pass them as props (reactive) or access them on the created or updated hooks (not reactive) to store in the component's $data. Read below for more details and each of the 3 cases.
A brief explanation of Vue 2 reactivity
(skip this if you don't need it)
The Vue reactivity basically relays on two intertwined objects: Watcher and Dep. Watchers have a list of dependencies (Deps) in the deps attribute, and Deps have a list of dependants (Watchers) in the subs attribute.
For every reactive thing, Vue instantiates a Dep that tracks reads and writes on it.
Vue instantiates a Watcher for every component (actually, for the render function) and every Computed Property. The Watchers watch a function during its execution. While watching, if a reactive object is read, the associated Dep notices the Watcher, and they become related: The Watcher.deps contains the Dep, and the Dep.subs contains the Watcher.
Afterwards, if the reactive thing changes, the associated Dep notifies all its dependants (Dep.subs) and tells them to update (Watcher.update).
When a component is destroyed, all its Watchers are destroyed as well. This process implies iterating each Watcher.deps to remove the Watcher itself from the Dep.subs (see Watcher.teardown).
The problem
All the components that depend on the same reactive thing insert a Watcher on the same Dep.subs. In the following example, the same Dep.subs contains 10,000 watchers:
1,000 items rendered (e.g. a grid, an infinite scroll, ...)
Each item implies 10 components: itself, 2 router-link, 3 buttons, 4 other (nested and not nested, from your code or third party).
All components depend on the same reactive object.
When destroying the page, the 10,000 watchers will remove themselves from the Dep.subs array (one by one). The cost of removing themselves is 10k * O(10k - i) where i is the number of watchers already removed.
In general, the cost of removing n items is O((n^2)/2).
Workarounds
In case you render many components, avoid accessing shared reactive dependencies on the render or computed properties.
Instead, pass them as props or access them on the created or updated hooks and store them on the component's $data. Bear in mind that the hooks aren't watched so the component won't be updated if the source of data changes, which is still suitable for many cases (any case where the data won't change once the component is mounted).
If your page renders a long list of items, the vue-virtual-scroller is bound to help. In this case, you can still access shared reactive dependencies because the vue-virtual-scroller reuses a small pool of your components (it does not render what is not seen).
Take into account that having thousands of component might be easier than you expect because we tend to write small components and compose them (actually a good practice)
Case: Vuex
If you do something like this in your render o computed property, your component depends on all the chain of reactive things: state, account, profile.
function myComputedProperty() {
this.$store.state.account.profile.name;
}
In this example, if your account does not change once the component is mounted, you can read it from the created or beforeMount hook and store the name on the Vue $data. As this is not part of the render function nor part of a computed property, there is no Watcher watching the access to the store.
function beforeMount() {
this.$data.userName = this.$store.state.account.profile.name;
}
Case: router-link
See the issue #3500
Case: Vue I18n
This has the same underlying problem but with a bit different explanation. See the issue #926

It's not a vue problem, see on your mixins/options.
Eg. i18n (my pain) in every of 200 components will show the same result. It removes a lot of watchers on beforeDestroy. Without i18n the list works 30 times faster.
How to fix it? Move the slow hook-handlers to the parent component and get needed data/methods from it.
The sample with i18n
Vue.mixin({
beforeCreate() {
if (this.$options.useParentLocalization) {
this._i18n = parent.$i18n;
}
},
});
Usage:
new Vue({
// i18n, <-- before
useParentLocalization: true,
components: {
Component1
}
})

Related

How to enable tracking deep changes in component props

I have a component who initialized like this
<custom :opts="{map: false}"></custom>
and there is HTML similar to this
<template id="custom">
<div v-if="opts.map">
I'm awesome
</div>
<button v-on:click="show"></button>
</template>
where
function show(){
this.opts = {map:true} // (1) <-- This is working and I could see hidden div
this.opts.map = true // (2) <-- For some reason not working
Vue.set(this.opts, 'map', true) // (3) <-- Still not working
}
So my question is why variant 2 doesn't work and what should I change to make my control react to value reset on a button click. Or a proper explanation why (1) is working, but (2) isn't - also will be accepted as an answer.
The real problem with the code (all 3 versions) is changing a component's property from within a component. In idiomatic Vue, only the parent should change properties. If a component needs to effect a change, it should emit an event to the parent and let the parent make the necessary changes. Otherwise, there is ambiguity in which component "owns" the property.
One Way Data Flow
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around.
Sending Messages to Parents with Events
Can be off base here but I believe this happens because in vue component props are not reactive, so their objects are not being observed in depth. Or rather they are "a little bit reactive", reassigning the root prop does cause the DOM update but is not expected to be done manually and you'll see a warning when doing such on development build:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "..."
And for as why props are not completely reactive in the first place: https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
To work around the whole issue you must pass any necessary props to the component data and if those props were passed as nested objects you might also want to completely avoid mutating them from within the component since it will propagate to the parent which, unless clearly mentioned, can be a source of bad news.

How much of this business logic belongs in Vuex?

I have a simple app which pulls products from an API and displays them on-page, like this:
I've added Vuex to the app so that the search results as well as the product search array doesn't disappear when the router moves the user to a specific product page.
The search itself consists of the following steps:
show loading spinner (update the store object)
dispatch an action to access the API
update the store object with products, spinner
decide if the product list is exhausted
hide loading spinner
You get the idea.
With all of the variables stored in Vuex, it stands to reason all of the business logic should belong there as well, but should it really?
I'm talking specifically about accessing store params such as productsExhausted (when there are no more products to display) or productPage (which increments every time the infinite scroller module is triggered) etc.
How much logic - and what kind - belongs in Vuex? How much does not?
I was under the impression that Vuex is used for storage only but since all of the data is located there, fetching it all back to the Vue app only to send it all back seems like an overly verbose way to address the problem.
Vuex allows you to share data !
For everything that concerns the state of the app its pretty straightforward.
All the data that can be used by multiple components should be added
to the store.
Now concerning the business logic, even though I find its not really clear in the official documentation, it should follow the same principle.
What I mean is that logic that can be used by multiple components should be stored in actions.
Moreover actions allows you to deal with async operations. Knowing this, your code that pulls the data should definitely be stored in vuex's actions.
What I think you should do is to put the request inside an action, then mutate the state of your variables and automatically your UI will reflect the changes.
Moreover, a good pattern to apply is to convert most of the logic to a state logic. For instance consider this demo of a jumping snowman. In here the click action results on updating a value from the store. Although the interesting part is that one component uses the watch functionnality to be notified when the store changes. This way we keep the logic inside the component but use the store as an event emitter.
var store = new Vuex.Store({
state: {
isJumping: 0
},
mutations: {
jump: function(state){
state.isJumping++;
}
}
})
Vue.component('snowman', {
template: '<div id="snowman" :class="color">⛄</div>',
computed: {
isJumping: function(){
return this.$store.state.isJumping;
}
},
watch: {
isJumping: function(){
var tl = new TimelineMax();
tl.set(this.$el,{'top':'100px'})
tl.to(this.$el, 0.2, {'top':'50px'});
tl.to(this.$el, 0.5, {'top':'100px', ease: Bounce.easeOut});
}
}
})

Inject reducer for on demand component which was not in the store or combined reducers initially

I'm trying to build some modular SAP so many teams can work separatelly.
Basically, I want my containers to be independent in terms of container, store, reducers, sagas.
The actual question is (example code):
I render a basic template:
<div>
<a onClick={emitLoadUserListAction}>Load user list</a>
<UserList/>
</div>
At this point, I make use of 1 reducer for UserList to keep the array of users (empty at the beginning).
Let's assume I have a saga, waiting for this data to come as a user list in a json.
Store:
{
UserList: []
}
Once the saga fetches the data, publishes an action modifiying the current store:
Store:
{
UserList: [{name:"john",counter:0},{name:"pepe",counter:0}]
}
Now my UserList component can list this as we have the mapStateToProps pointing to this part of the store.
this.props.userList.map ( (userData,i) => { return <User data={userData}> } ))
So now everything is working like a charm if User component is just a normal component.
But what if User is actually a container, which is expecting to work on its own, with its own state I didn't connected yet via its own reducer. I don't want his parent to manage it. I want user to be independent as I could pass its location in the store with reselect selector or similar, or I could just pass the index in the array as a prop, so I could be the selector. This way I would have store injected in props, but I won't have reducer.
I'm pretty sure many of you already pass through this but I couldn't find a proper answer.
As you can see the idea is to have a component, which is loading on demand, not in the initial combineReducers, not handled by its parents, just render, and reducer injected to work on its own.
If I could have just a way to load its reducer on demand then, I would not store the data in the UserList but it will be a composition of reducers.
Thanks a lot in advance.
I'm continuing on from my comment and the question that followed so I can expand on it without the restrictions of the comments section.
Yes, my library calls replaceReducer on the store to in order to, well, replace the reducer with the new one included. In order to do so, I provide a Higher-Order Component (HOC) which bundles the component with it's associated reducer and performs the replacement when it is mounted.
The interface looks something like this:
export const MyBundledComponent = bundle(MyComponent, myReducer)
The only requirement for it to work is that the component is mounted within a Provider from react-redux. This gives the HOC access to the store on React's context the same way the connect HOC does. This isn't really a very prohibitive restriction though, as most redux apps have a Provider at the top of the tree already.
Hope this helps.
So far I found resources like this:
https://medium.com/#jimmy_shen/inject-reducer-arbitrarily-rather-than-top-level-for-redux-store-to-replace-reducer-fdc1060a6a7
which allow you to inject reducers on demand by replacing the main reducer by using the Redux store API store.replaceReducer(nextReducer)
The problem with this solution is the need to have access to the main store object from the child component that should be encapsulated.
For the moment working not ideal solution that I found is to deliver the encapsulated component with a "multiple components reducers" meaning that the reducer assumes there could be more than one component under the same parent where each one has different ids.
So each action should check the payload ID, in order to get the state from the store object.
This would mean a small change in the hierarchy as the component would not be child but sibling.
Following the previous example, imagine that we list a shallow version of the user list and then you show more data once u click on any user:
`
Store: {
UserList: [], // basic info, id plus minimal data
users: {} --> userReducer // listing each user by key
}
`
This way the user component will expose multiUserReducer instead of logic for just one.
This obviously means the reducer is loaded in advance, even if you never load any user componet.

Is my React component being recreated instead of updating?

I am trying to combine Angular and React.js. I have an work example project here I have seen a couple of ways to bring the Angular and React.js together. One of the methods I have seen is to create a directive and create the React component in the link function. For example in the first part of the project to generate the React version(in red) I am using
.directive('reactElementRepeater', function() {
return {
link: function(scope, element) {
var update_react = function(oldVal, newVal){ //Called every time one of the two values change
React.renderComponent(Demo_Element({
numberOfElements: scope.myModel.numberOfElem,
numberInElements: scope.myModel.numberInElem
}), element[0]);
}
scope.$watch('myModel.numberOfElem.length', update_react);
scope.$watch('myModel.numberInElem', update_react);
}
}
});
What I want and what should happen in a React enabled application is for something in the model to be updated, then that update is sent through React and it will alter the DOM as little as possible to reflect that change. It looks like that instead of updating a bit of the DOM this will Create a new React component each time with renderComponent.
React.renderComponent() instantiates the root component, starts the
framework, and injects the markup into a raw DOM element, provided as
the second argument.
Is it actually recreating the elements each time? If that is the case is there a way to alter this so that doesn't happen?
Just to be clear I know about ngReact, I just want to know other ways to speed up Angular with React.
Yes this is fine, it's not mounting the component multiple times.
When you call React.renderComponent() the second argument is the element which react should render the component to. So react notices if you are rendering the same component to a dom element that already contains a mounted instance of the component, and does not re-mount the entire component, it just updates the properties of it instead.
You can see this in action if you make a component with componentDidMount function defined. You'll notice that componentDidMount will only execute the first time renderComponent gets called. And afterwards, subsequent calls to renderComponent on the same target dom element will not call it because the component is already mounted. Likewise getDefaultState and getDefaultProps also only get called on the first renderComponent call.
If you're asking will the render function of the component be called every time the answer is yes. But this is how react works, you want the render function to get called because props might have changed. You can block it from being called by using shouldComponentUpdate (http://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate) and returning false. However react developers recommend you don't use this to block render calls unless you have specific performance problems - most of the time it should be fine to just let the render call execute as it wont cause any slow dom updates unless things have actually changed.

Idiomatic way to listen to model changes

I'm playing with React for the first time and I think I really like it. I've implemented (large parts of) the board game Go with it and so far, but I've run into something strange that I don't know how to approach in the idiomatic React way. Basically, I've got a model--the board game--implemented as its own class Board. It exposes only it's constructor, and methods play(i,j) and pass. It handles all of the game logic and updates its own internal state appropriately. It has no reference to anything related to a view/component. I've got a React Component called BoardView which maintains a reference to an instance of a Board. I've also got a Component called AlertView that displays messages about the game state (illegal moves and such) when appropriate.
Everything works well now, and I like the separation of concerns between the Board class and its views. However, the way I have my Board class communicate its changes to the views is unusual, and I feel that it is inconsistent with other React code. Basically, I abuse jQuery's event system to allow me to trigger arbitrary events like ["update", "atari", "suicide"]. In this scheme, the Component has an onClick listener that calls Board.play, which triggers 0 to many events on the Board instance. The Component listens for an "update" event, and calls this.setState, which will force it to re-render(), putting the view into a state that correctly depicts the game. The AlertView listens for the "atari" and "suicide" events on the same board instance and similarly calls this.setState, which triggers another render().
Should I cut out the jQuery events? If so, what's the best way of doing this?
All code is available here and you can play with the app here.
Edit:
For posterity's sake, this question was asked at commit 3f600c.
I'm not sure if this is idiomatic React, but from the React tutorial, the onSubmit handler is passed from the parent to the children as a props.
In your case that would mean to pass the onPlay handler from BoardView to BoardIntersection like this:
var BoardView = React.createClass({
getInitialState: function() {
return {"board": this.props.board}
},
playHandler: function(i, j) {
this.props.board.play(i, j)
},
render: function() {
...
intersections.push(BoardIntersection({
color: this.state.board.board[i][j],
row: i,
col: j,
onPlay: this.playHandler
}));
...
}
})
and BoardIntersection will call onPlay as needed:
var BoardIntersection = React.createClass({
handleClick: function() {
this.props.onPlay(this.props.row, this.props.col);
},
})
tungd's comments pointed me in the right direction, but I decided to answer my own question for a more complete answer.
I ended up removing all of the custom events being fired on the model. I found the following snippet from the React docs to be especially helpful:
A common pattern is to create several stateless components that just render data, and have a stateful component above them in the hierarchy that passes its state to its children via props. The stateful component encapsulates all of the interaction logic, while the stateless components take care of rendering data in a declarative way.
Instead of firing events like "atari" and "suicide" on the model, I just set boolean properties on the model in_atari and attempted_suicide. Now, only one "parent" Component in my application has state. It renders all sub-components via declarative props. The AlertView is one such sub-component whose render method now checks the new boolean flags to render the appropriate text. The main parent Component passes a handler to its sub-components that updates the component state (and subsequently forces a re-render).
In the relevant commit, I've named the parent component ContainerView.

Categories