The Problem:
Every time I navigate between the list page (home) and detail page my site starts to slow down drastically. With this I mean, after like 3 times going back and forth I start noticing it having hiccups, after 5-6 times my whole pc starts to freeze.
My Project:
It's a Vue Project, with currently only 2 routes. The homepage is a list of items and a detail page for every item on the list. The detail page (specifically the tree component) is probably where the issue is because when I remove this, the problem is gone. I put some code at the bottom of this post with the basic structure of the project.
What I'm looking for:
Since I'm not getting any errors, I'm not sure where the problem is here. There is probably something I can do better in the way my project is set up, the way I load/show things. So I'm looking for ways to find out where the problem is at.
What I tried:
Stay Alive
While searching for solutions I came across the <stay-alive> tag. I tried putting this around my <router-view>. This does get rid of the slowing down, but I also lose all my dynamic content. The data on all pages is now the same when I navigate between different detail pages.
Data Fetching (https://router.vuejs.org/guide/advanced/data-fetching.html#fetching-after-navigation)
I was thinking, maybe it helps if I load all the data before someone enters a route or do some kind of loading before I show the page. This did not help.
It's possible that one of these things is the right direction, and I just didn't implement it right. Not 100% confident with my coding yet :)
views/home.vue
Just a simple page with a list of items that link to the detail page
<template>
// list with items that link to their corresponding detail page
</template>
import { mapState } from 'vuex'
export default {
name: 'Home',
computed: {
...mapState([
'builds'
])
}
}
views/details.vue
Now, this page is a little more complex. The big thing on this page is a canvas that is generated with Pixi Js, this canvas changes while the user is scrolling through the page. The Canvas element is its own component, so I pass some data with a prop.
<template>
<div class='page-wrapper'>
<div class="content-container">
<section class="level milestone" v-for="level in build.guide" :key="level.id" :id="level.level">
// Some stuff to display
</section>
<div class="sidebar">
<tree :myprop="current"></tree>
</div>
</div>
</div>
</template>
import { mapState } from 'vuex'
import Tree from '#/components/tree'
export default {
name: 'Build',
watch: {
currentActive: {
// To know where to user currently is
// Pass this data to my tree component
}
},
computed: {
...mapState([
'builds'
])
}
}
components/tree.vue
This is where my canvas is drawn with the help of data from a JSON file.
<template>
<div id="tree">
</div>
</template>
import axios from 'axios'
import * as PIXI from 'pixi.js'
import { Viewport } from 'pixi-viewport'
export default {
name: 'Tree',
props: ['myprop'],
data () {
return {
app: new PIXI.Application({ transparent: true, antialias: true }),
treeData: {},
// Some more
}
},
watch: {
myprop: function (newVal) {
// Some stuff with my prop
}
},
created () {
axios.get('/data/data.json', {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
this.treeData = response.data
this.loadTree()
})
.catch(function (error) {
console.log(error)
})
},
methods: {
loadTree () {
// Load images and draw the canvas
PIXI.loader
.add('Image', require('#/assets/image.png'))
.load(this.drawTree)
},
drawTree () {
// Do all the drawing on the canvas
}
}
}
Oke, so i've kind of solved it for now. I'm still new to all this, but i started to look more into working with dev tools so i can find out where issues like this come from. I think i still can win a lot with this, but for now it helped to destroy my PIXI app from data when i'm done with it.
beforeDestroy () {
this.app.destroy()
}
Maybe some usefull links if someone ever finds this thread that has similar issues:
Using dev tools to find memory leaks:
https://www.youtube.com/watch?v=Hr2vrhrNaRo
https://www.youtube.com/watch?v=RJRbZdtKmxU
Avoiding memory leaks in vue:
https://v2.vuejs.org/v2/cookbook/avoiding-memory-leaks.html
Related
I am creating dynamic pages using Nuxt. In the pages folder I have one file _url.vue. It contains the following code:
<template lang="pug">
div
component(
v-for="component in components"
:key="`${component.type}-${component.id}`"
:is="`the-${component.type}`"
)
</template>
<script>
// import vuex
export default {
computed: {
...mapGetters('app', {
components: 'getComponents'
})
}
}
</script>
setComponents happens at the middleware level:
export default async function ({ store }) {
await store.dispatch('app/setPage')
}
In the first milliseconds of page load, the content "jumps" as the components are rendered on the fly. How can this situation be corrected?
I'd first try to import the components manually, to see where this all comes from: the components taking some time to get injected or the layout being displayed, just to be sure.
Then, I had a discussion about it here, you may give it a look: Vue: wait to render until all components are mounted
There are several ways of handling this kind of micro-jumping explained there. You can choose your own solution. Also depends if you're using your app as universal or SPA only.
Looks like require is a way to go but some alternative are also available.
I am working on a MERN application where people can add user created content. I have incorporated a basic loading functionality to give the user feedback when fetching data from the server. However I am hoping for some improvements for better performance and reducing loading times.
My current approach:
As an example I will explain my PostComponent which will request user created posts from the database through a dispatched redux action (getPosts). The posts are rendered in a child component PostFeed and subsequently mapped into individual PostItem components.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import PostForm from './PostForm';
import PostFeed from './PostFeed';
import Spinner from '../common/Spinner';
import { getPosts } from '../../actions/postActions';
class Posts extends Component {
componentDidMount() {
this.props.getPosts();
}
render() {
const { posts, loading } = this.props.post;
let postContent;
if (posts === null || loading) {
postContent = <Spinner />;
} else {
postContent = <PostFeed posts={posts} />;
}
return (
<div className="feed">
<div className="container">
<div className="row">
<div className="col-md-12">
<PostForm />
{postContent}
</div>
</div>
</div>
</div>
);
}
}
Posts.propTypes = {
getPosts: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post,
});
export default connect(mapStateToProps, { getPosts })(Posts);
Before the posts are loaded, I am setting a boolean loading to true. After getting all the posts, loading is set to false again. The if statement in the component above is rendering a spinner which is a .gif file or the PostFeed component depending on the state of the loading boolean.
postActions:
...
export const setPostLoading = () => {
return {
type: POST_LOADING,
};
};
export const getPosts = () => (dispatch) => {
dispatch(setPostLoading());
axios
.get('/api/posts')
.then(res => dispatch({
type: GET_POSTS,
payload: res.data,
}))
.catch(err => dispatch({
type: GET_POSTS,
payload: null,
}));
};
...
postReducer:
...
export default function (state = initialState, action) {
switch (action.type) {
case POST_LOADING:
return {
...state,
loading: true,
};
case GET_POSTS:
return {
...state,
posts: action.payload,
loading: false,
};
...
This all works fine this way but I have two concerns about this method:
First, using spinners while loading components makes things jump around on the page a bit. While not necessarily a bad thing, I like to create the most seamless user experience and find implementing placeholders a more elegant solution.
Placeholder Loading Example
How do I achieve implementing placeholder items for each separate component so content does not jump around and stays nicely postioned while loading?
Second, When loading the posts, it is loading all available posts that are stored in the database. I can imagine when having excessive amounts of posts, loading all the individual PostItems might take too long and take a lot of unnecessary bandwidth.
How could I add the functionality to only load the content displayed in the browser window so more are loaded when the user scrolls down? Probably by scroll eventhandler? But how does it decide which PostItem to render and which to keep as placeholders?
Hope I explained it clear enough, still relatively new to react/redux so there could some mistakes here. Any tips, suggestions or best practices are welcome.
Thanks a lot!
You are asking a number of questions here. First, it sounds like you've come to grips with needing your "skeleton" to display for each component, and the good news is there are several solutions available such as react-content-loader which uses customizable SVG, react-placeholder, (or you can roll your own using react-transition-group. The documentation will explain how you manage the transition per component as you requested.
With respect to the "excessive amount" of items coming back from your axios API call, it's hard to say without a knowing a rough number (100s, 1000s, more, etc.) and if you've found a performance issue with how much data you're returning. If the number ends up being problematic, what you are looking for is called "virtual (or infinite) scrolling" and pagination where you're not requesting ALL of the data at once, but rather in chunks. For example, StackOverflow uses paging, the new Reddit uses infinite scroll. Again, there are NPM packages for that too such as react-tiny-virtual-list.
I have a Vue component that's kept alive using Vue's element for caching purposes. However, the problem I am having right now is that once I sign out of one account and create a new account on my Vue application, the component I'm "keeping alive" is being reflected for the new user (which obviously isn't relevant for the new user).
As a result, I want to destroy that component once the user signs out. What is the best way to go about this?
I've managed to solve my issue in the following way. Essentially, if the user is logged in, keep the dashboard alive. Else, don't keep the dashboard alive. I check if the user is logged in or out every time the route changes by "watching" the route (see below). If you are reading this and have a more elegant solution - I'd love to hear it.
The following is the code for my root component
<template>
<div id="app">
<!-- if user is logged in, keep dashboard alive -->
<keep-alive
v-bind:include="[ 'dashboard' ]"
v-if="isLoggedIn">
<router-view></router-view>
</keep-alive>
<!-- otherwise don't keep anything alive -->
<router-view v-else></router-view>
</div>
</template>
<script>
import firebase from "firebase";
export default {
name: 'app',
data() {
return {
isLoggedIn: false // determines if dashboard is kept alive or not
}
},
watch: {
$route (to, from){ // if the route changes...
if (firebase.auth().currentUser) { // firebase returns null if user logged out
this.isLoggedIn = true;
} else {
this.isLoggedIn = false;
}
}
}
}
</script>
I had the same problem and I solved it by using an array of cached components and bus event.
Here is my HTML keep-alive App.vue:
<keep-alive :include="cachedComponents">
<router-view></router-view>
</keep-alive>
Here is what I'm doing in the created() life cycle:
created() {
// Push Home component in cached component array if it doesn't exist in the array
if (!this.cachedComponents.includes('Home')) {
this.cachedComponents.push('Home')
}
// Event to remove the components from the cache
bus.$on('clearCachedComponents', (data) => {
// If the received component exist
if (this.cachedComponents.includes(data)) {
// Get the index of the component in the array
const index = this.cachedComponents.indexOf(data)
// Remove it from the array
this.cachedComponents.splice(index, 1)
}
})
}
And inside another component just trigger the event and send the component to remove in parameter.
Another.vue
bus.$emit('clearCachedComponents', 'Home')
If you don't know how to make a bus event there are lot of tutorials on the internet like this to do that. But bus event is my way to do that and you can use everything you want like a child emitter or Vuex. That I want to show is to use an array of components to manage your cache. All you have to do is to add or remove your components in the array.
for anyone looking for a solution that destroys the cache
in my case I was using this in a logout route, replace router.app with this.$root in Vue instances and the $children index/nesting may differ for your app
setTimeout(() => {
var d = [];
for(var vm of router.app.$children[0].$children) {
if(vm._inactive === true)
d.push(vm);
}
for(var vm of d) {
vm.$destroy();
}
});
If your problem is that the component is still holding the old user's data, the only option is resetting it with an internal reset function, which reloads the data for the new user one way or another.
See:
http://jsfiddle.net/paolomioni/hayskdy8/
var Home = Vue.component('Home', {
template: `<div><h1>{{ title }}</h1>
<input type="button" value="click to change text" v-on:click="title = Math.random()"">
<input type="button" value="click to reset component" v-on:click="reset"></div>`,
data: () => {
return {
title: 'BBB'
}
},
methods: {
reset() {
this.title = 'BBB'
}
}
});
In the fiddle, click on the button "change text" to change the text: if you click the checkbox twice to switch view and back again, you will see that the number you've generated is still kept in memory. If you click on the "reset" button, it will be reset to its initial state. You need to implement the reset method on your component and call it programmaticaly when the user logs out or when the new user logs in.
I've been struggling with this for a few days and was hoping for a graceful way of handling dynamic URLs with no data.
I have the following routes:
const router = new VueRouter({
routes: [
{path: '/product/:slug', component: Product},
{path: '/404', component: PageNotFound, alias: '*'}
]
});
In the Product component, I have an object of products and, depending on the slug variable, load the product to show.
The issue i'm having is when the URL is a slug that does not exist in the products dataset. I would like to load the PageNotFound component, without updating the URL.
Is this possible? It would be nice to have a consistent 404 page throughout the app and would also be good for me not to have to repeat myself with a v-if in the product table.
The closest I've got to it is this:
if(!product) {
this.$router.replace({path: '/404', query: {product: this.$route.params.slug}});
}
However, this updates the actual URL which is not very good UX.
Any clues?
You could conditionally render your PageNotFound component in Product.vue if the query returns no results, and then not have to fiddle with your router at all.
Thanks to Kyle pointing me in the right direction, this is what I came up with.
Becuase I am being slightly unorthodox and using server-side components and JavaScript, I already had my page not found component loaded - which looks like this:
const PageNotFound = {
name: 'PageNotFound',
template: `<div>
<h1>404 Page Not Found</h1>
<p>Head back to the <router-link to="/">home page</router-link> and start again.</p>
</div>`
};
I made sure the PageNotFound.js file was loaded in the HTML before my product component, so I was able to do the following:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product"><h1>{{ product.title }}</h1></div>
<page-not-found v-if="notFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
notFound: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
if(!product) {
this.notFound = true;
}
}
return product;
}
}
};
Things to note in the above:
Data is being loaded asynchronously, hence the check to see if products exist
The PageNotFound component is loaded in - this is ES6 for PageNotFound: PageNotFound - Vue then automatically makes a <page-not-found></page-not-found> element
That element then has a v-if which gets triggered. As the first container would not be in existence if there is no product, only the 404 component is displayed
I don't do it based on product, as you would get a flash of the 404 if the product data was still loading via an API.
It's better practice to have the URL params as props (see docs), which I will be doing at some point!
To conclude, this allows you to show a consistent 404 page throughout your SPA (single page application) while maintaining URLs with dynamic routes. It allows you to load another component or show another component without updating the URL and also lets you have a wildcard 404 for dynamic routes.
Hope that all makes sense and helps someone in the future and saves them from wasting ~4 hours of trial, error and googling. (and yes I have "keyword" and phrase stuffed this answer to help someone find it...)
This question already has answers here:
How can I bind the html <title> content in vuejs?
(9 answers)
Closed 5 years ago.
I'm building a web application using vue.js v1.0, vue-router v0.7 and WebPack. I'm following Single File Component pattern and have different components for each page.
I don't know how I could change page title in different routings (or maybe different components) when I am navigating through the web app pages. I also want page titles to be available in browser history.
In addition to my earlier solution posted here, there is a second method that I found after a bit of research: use Navigation Guards
As detailed in my previous answer, here is the problem: vue-router may start reusing route components after getting created for the first time. There is really no need to destroy these components on route-exit, and re-create on subsequent route-entry. Therefore the created hook in my earlier solution may not fire on subsequent visits to the same route. Therefore our window title may not work as expected.
To overcome that problem, we can set the window title on a route-change event. The router instance has a afterEach hook that gets called after route change. This can be used to set window title as detailed below:
// Let's say this is your router instance, with all "Named Routes"
const ROUTER_INSTANCE = new VueRouter({
mode: "history",
routes: [
{ path: "/", name: "HomeComponentName", component: HomeComponent },
{ path: "/about", name: "AboutComponentName", component: AboutComponent },
{ path: "*", name: "RouteErrorName", component: RouteError }
]
})
// Assign page titles for each of the "named routes"
// Reason: This allows us to have short named routes, with descriptive title
const PAGE_TITLE = {
"HomeComponentName": "Your Dashboard",
"AboutComponentName": "About Us Page",
"RouteErrorName": "Error: Page not found"
}
ROUTER_INSTANCE.afterEach((toRoute, fromRoute) => {
window.document.title = PAGE_TITLE[toRoute.name]
console.log(toRoute) // this lets you check what else is available to you here
})
This may still not help you if you are navigating between similar routes, like "/user/foo" to "/user/bar". If you want user name in the titlebar or some dynamic page specific info, check out Reacting to Params Changes as detailed in http://router.vuejs.org/en/essentials/dynamic-matching.html. Based on docs, we should be able to use watch in component as follows:
watch: {
'$route' (toRoute, fromRoute) {
window.document.title = "some page title"
}
}
Hope it helps!
I also had the same problem few days ago, and I resolved as follows in my route component definition:
export default {
created: function() {
window.document.title = "Page Title for this route"
...
},
...
}
That's really not the correct way of doing it. Reason: I am making a big assumption that the route component gets created everytime on changing to a new route. It is true in vue-router for now, but may change in future.
I was using ui-router in Angular 1.4 earlier, which allows route components to live in memory (sticky states), so that the route change is instantaneous next time. If vue-router ever implements something similar to sticky states, my above method of setting title in created hook will fail.
But till that happens, you may use this solution.
I've got a solution and used it on one my projects.
First create a directive.
Vue.directive('title', {
inserted: (el, binding) => document.title = binding.value,
update: (el, binding) => document.title = binding.value
})
Suppose, we are working on 'MyComponent.vue' file.
Then use that directive on the router-view component.
<router-view v-title="title" ></router-view>
export default {
data(){
return {
title: 'This will be the title'
}
}
}
This works even if the component is updated or the page is reloaded.
Worked very well for me!!