Rendering a lot of components - vue.js

I am trying to make a small application with VueJS and I am having some performance issues.
I'm not sure that this is something I can do something for, but asking anyway.
My goal is to render a lot of components (more than 10,000 at the same moment). But the time it takes to Vue to render thoses components and update the dom is quite long.
Here I made a small JsFiddle so you can try on your side: https://jsfiddle.net/rt5cjbby/
At home, with a badass i7 processor, it takes 400ms to render. Which is, much more when you test with more components.
My question is: Is there any technique I could use to have better performances?
Is it possible to update the DOM every 100 components to be rendered? The client could see the first 100 components very quickly then the browser would continue to render....
Here is my example code if you don't want to open jsFiddle:
some_table = []
for (let i = 0; i < 2000; i++) {
some_table.push({
foo: 'bar'
})
}
let someComponent = Vue.extend({
template: '#some-component',
props: ['input']
})
new Vue({
el: '#app',
data: {
lines: some_table
},
components: {
someComponent
}
})
And my templates:
<div id="app">
<div v-for="line in lines">
<some-component :input="line"></some-component>
</div>
</div>
<template id="some-component">
<div>
{{ input.foo }}
</div>
</template>
Of course, I could fill my table every 0.01s but I don't feel that this is the best solution.
PS: I accept responses like "[Angular|React] are better for your usecase because ...."
Thank you very much for your help/experiences/advices and have a nice day

If you are interested in only rendering the component's end result, you can easily achieve exceptional performance utilizing the render function.
Here is a complete working Fiddle of the following:
Template:
<div id="app"></div>
Javascript:
some_table = []
for (let i = 0; i < 10000; i++) {
some_table.push({
foo: 'bar'
})
}
new Vue({
el: '#app',
data: {
lines: some_table
},
render: function (createElement) {
if (this.lines.length) {
return createElement('div', this.lines.map(function (line) {
return createElement('div', line.foo)
}))
} else {
return createElement('p', 'No items found.')
}
}
})
setTimeout(() => {
for (let i = 0; i < 10; i++) {
some_table.unshift({
foo: 'bar stool'
})
}
}, 2500)
In this example, 10,000 "cells" are rendered almost immediately (about 450ms while profiling on my machine). After a 2.5 second delay, an additional 10 new records are added as well, since the render function will respond to changes to the array. This allows you to make changes to the rendered state as needed by modifying the source array.
Note that you can still perform complex 2-way data binding via your own v-model implementations in render functions, although it is significantly harder to maintain.

Related

How to solve filtered API results in Vue app returning undefined?

As a newbie I’m trying to get my head around Vue and I’m having difficulty with the functionality of my Github jobs api app. The full project can be viewed here https://codesandbox.io/s/modest-banach-u6fzg
I’m having issues with the filtering of the api results, in particular the ‘load more’ button which, ideally, will filter the results of the api into batches of 10. The issues are:
The load more function works initially, but once there are, say, 30 results displayed on the app, the search function at the top does not work.
The search function works on the initial render of the page with 10 results being displayed, but the ‘load more’ button/function does not work on the returned results. As an example, if you search for ‘UK’ in location you get an initial 10 results, but a console.log reveals that there are 50 results returned from the api, which come back as undefined so are not displayed.
I’m not sure if these two problems are linked to a single issue.
Any advice would be much appreciated, as well as any feedback on how I’ve implemented the app.
Thanks!
Mike
You need to use computed prop instead of a function and you also you should reset loaded items counter if jobs loaded again.
See modified code
<template>
<div v-if="jobs.length">
<div v-for="job in filterJobs" v-bind:key="job.id">
<!-- <div v-for="job in jobs" v-bind:key="job.id"> -->
<Job v-bind:job="job" />
</div>
<button v-on:click="loadMore">Load More</button>
</div>
</template>
<script>
import Job from "./Job";
export default {
name: "Jobs",
components: {
Job,
},
data() {
return {
jobCount: 10,
};
},
props: ["jobs"],
watch: {
// watcher to reset a counter
jobs(newValue, oldValue) {
this.jobCount = 10;
},
},
computed: {
// computed prop instead of function, that way it would be reactive
filterJobs() {
return this.jobs.slice(0, this.jobCount);
},
},
methods: {
loadMore() {
// we need to check if we are exceeding a length of jobs array or not
if (this.jobCount + 10 <= this.jobs.length) {
this.jobCount += 10;
} else {
this.jobCount = this.jobs.length;
}
},
},
};
</script>
<style lang="stylus" scoped></style>

VueMapbox trying to create multiple markers

I am trying to create multiple markers in Vue using VueMapbox. Currently the map displays correctly but there is only one marker. I think there is something wrong either with my v-for statement or perhaps in the forEach statement. I am trying to place a marker on each location but only the first location is added.
Here is the code for my vue component:
<template>
<MglMap
:accessToken="accessToken"
:mapStyle.sync="mapStyle"
>
<MglMarker v-for="coordinate in coordinates" :key="coordinate" :coordinates="coordinates">
<MglPopup>
<VCard>
<div>{{ country }}</div>
<div>{{ cases }}</div>
</VCard>
</MglPopup>
</MglMarker>
</MglMap>
</template>
<script>
import Mapbox from "mapbox-gl";
import { MglMap, MglPopup, MglMarker } from "vue-mapbox"
export default {
name: 'Map',
components: {
MglMap,
MglPopup,
MglMarker,
},
data() {
return {
accessToken: 'pk.accesstoken...blahblahblah',
mapStyle: 'mapbox://styles/mapbox/dark-v10',
coordinates: [],
country: '',
cases: 0,
}
},
created() {
this.mapbox = Mapbox;
this.getData();
},
methods: {
getData: function () {
fetch('https://coronavirus-tracker-api.herokuapp.com/v2/locations')
.then(response => response.json())
.then(data => {
const locations = data.locations;
locations.forEach(stat => {
const country = stat.country;
this.country = country;
const cases = stat.latest.confirmed;
this.cases = cases;
const coordinates = [stat.coordinates.longitude, stat.coordinates.latitude]
this.coordinates = coordinates;
})
})
}
}
}
</script>
You're currently doing a v-for on coordinates. It should be on locations.
If locations don't have all the required props a MglMarker needs, transform them in the forEach but that's all you should do in that forEach (if you need it at all). Don't use it to populate this.country, this.cases or this.coordinates. You only want to set those when a marker is clicked (if, and only if, you have any functionality listening to changes on those Vue instance properties).
There might be more details which need to be fixed but, without a minimal reproducible example it's very difficult to spot them. Note: you'll need to create a mapbox public token with readonly permissions for your example to work.
To summarize: Move the functionality from your forEach into a function called showMarker or activateMarker. Call that function whenever a marker is clicked or, if that's what you want, call it on one of the locations to make it the currently active one.
What your code does now is: it makes all markers the currently active one, therefore only the last one iterated will be currently active.
Here's what your MglMarker iterator might look like:
<MglMarker v-for="(l, key) in locations"
:key="key"
:coordinates="l.coordinates"
#click="activateMarker(l)"
>
<MglPopup>
VCard>
<div>{{ l.country }}</div>
<div>{{ l.latest.confirmed }}</div>
</VCard>
</MglPopup>
</MglMarker>
In activateMarker method you can dispatch any action to let the rest of the app know about your selection. Or you can close any other open popups, for example (although that can be handled easier with :closeOnClick="true" on each MglPopup).

How to implement my own upload file component and make it customizable in vue js?

I'm working on a project which expects a lot of places where I have to implement upload file component with different styles. I want to create highly customizable component which design I can modify easily and also I don't want to repeat myself and want to extract all the common logic into one place.
At this moment I have a vue.js version 2.2.2 and bulma css framework. I have a basic implementation of this component with only one design available. It supports a few states which represents current upload status:
component is waiting for the input
upload started
upload finished successfully
upload failed
Also this component is responsible for the upload process.
At this my component has a lot of responsibilities:
1. it knows how to deal with statuses:
<p v-if="isWaiting">
...
</p>
<div v-if="isLoading" class="is-loading">
<p class="title">{{currentPercent}} %</p>
</div>
...
data() { return {
currentStatus = Statuses.waiting
}}
computed: {
isWaiting() {
return this.currentStatus === Statuses.waiting;
},
...
}
it knows how to upload the data and count the current percent of data which is already transfered:
selectedFileChanged: async function(event) {
if (event.target.files.length === 0) return;
this.currentStatus = Statuses.uploading;
this.selectedFile = event.target.files[0];
const formData = new FormData();
formData.append("file", this.selectedFile);
try {
const result = (await axios.post("some url", formData, {
onUploadProgress: progress => {
const loaded = progress.loaded;
const total = progress.total;
this.currentPercent = Math.floor((loaded * 100) / total);
}
})).data;
this.currentStatus = Statuses.uploaded;
this.$emit("fileUploaded", {
file: this.selectedFile,
payload: result
});
} catch (exception) {
this.currentStatus = Statuses.error;
}
}
it has only one style which I can use
you can find the full code here: https://codesandbox.io/s/vue-template-gz2gk
So my question is: how to build this component to have an opportunity to change it's style easily and how to deal with upload statuses?
It seems to me that:
1. the component shouldn't know that axios is and how to upload the data;
2. the component should only be responsible for the current status and how to display it
I can introduce a new upload service which will know how to upload the data and add a new prop (current status) for upload file component and change it from the parent component. But in this case I will write the same code for all instances of the component.
Does someone know best practices of how-to create such customizable component?
UPDATE 1
I've tried to implement this functionality using slots and ended up with: https://codesandbox.io/s/vue-template-pi7e9
The component still knows how to upload the data, but now I can change the style of upload component.
So the new question is: how to work with slots and do not transfer a lot of variables and how to deal with uploading. I don't want my component to know how to upload the data :(
I've finished the component in the followig way:
In my parent component I have two different styles for my component:
<div id="app" class="container box">
<upload-file url="url"></upload-file> <-- the default one with statuses outside -->
<upload-file url="url"> <-- custom one which look like a box with statuses in it -->
<template #fileselect="{status, change}">
<custom-file-upload :status="status" #change="change"></custom-file-upload>
</template> <-- I've used custom-file-upload component here and tnjected all the properties from default implementation -->
</upload-file>
</div>
My default file input is nothing but a slot with default implementation:
<template>
<div>
<slot name="fileselect" :change="selectedFileChanged" :status="status">
<input id="fileselect" #change="selectedFileChanged" class="file-input" type="file">
<div class="help is-info" v-if="isWaiting">Waiting</div>
<div class="help is-success" v-if="isUploaded">Uploaded</div>
<div class="help is-info" v-if="isUploading">Uploading {{currentPercent}} %</div>
<div class="help is-error" v-if="isFailed">Failed</div>
</slot>
</div>
</template>
and what is the code looks like:
name: "upload-file",
props: ["url"], // url which will be used in upload request
data() {
return {
currentStatus: 1,
selectedFile: null,
currentPercent: 0
};
},
computed: {
someOtherProperties,
status() {
return {
isWaiting: this.isWaiting, // this.currentStatus === 1
isUploaded: this.isUploaded,
isUploading: this.isUploading,
isFailed: this.isFailed,
currentPercent: this.currentPercent,
selectedFile: this.selectedFile
};
}
},
methods: {
selectedFileChanged: function(event) {
this.selectedFile = event.target.files[0];
const xhr = new XMLHttpRequest();
// some handlers for XHR
xhr.open("POST", this.url, true);
xhr.send(formData);
}
}
Now I can use the file upload component with different styling but it will encapsulate in a base implementation all the status handling and upload logic.
I hope this solution will help someone :)
To view the full version of the code, please follow https://codesandbox.io/s/vue-template-pi7e9

How to access props data?

I have an PHP var used in a blade template and want to pass it to a vue's method.
I'm still learning so sorry if it seems obvious but I read the docs but found noting useful.
So I have this piece of code in my HTML
<chat-messages :messages="messages" :surgery_id="{{ $surgery->id }}"></chat-messages>
And in my JS
Vue.component('chat-messages', require('./components/ChatMessages.vue'));
const app = new Vue({
el: '#chat',
methods: {
fetchMessages() {
axios.get('/messages/').then(response => {
this.messages = response.data;
});
},
}
});
And I want to use something like axios.get('/messages/' + surgery_id).then(...)
But I can't figure out how to retrieve this surgery_id variable
In my ChatMessages.vue, I well created the properties
<template>
//Stuff to loop & display
</template>
<script>
export default {
props: ['messages' , 'surgery_id']
};
</script>
Use this as you do normally with the data:
axios.get('/messages/' + this.surgery_id).then(...)
You can access all the property of data option,props, and methods using this as context.
Further, if you want to use ES6, then it's even easier without concatenating them: (using tilde key `)
axios.get(`/messages/${this.surgery_id}`).then(...)
As per your query, you also need to pass props in your instance:
const app = new Vue({
// ...
propsData:{
surgery_id: 'your id value'
}
See my another post for more help.

Vuejs transition duration auto?

I'm using the Vuejs official router, and am currently working on giving different routes different animations, like in the example here: https://router.vuejs.org/en/advanced/transitions.html (see 'Route-Based Dynamic Transition')
My problem is that some transitions need a specific transition durations, and others don't (those I want to specify in the CSS).
I've made a variable that holds the duration to pass to the router, (same as this.transitionName in the router) but I was wondering if there was a way to set this variable to 'auto', for the routes that don't need a duration?
I don't know enough about your setup, so I'll only address modifying the transition duration.
As you can tell, the transitions are using css to manage the transitions.
I've taken the example in the doc you linked (https://github.com/vuejs/vue-router/blob/dev/examples/transitions/app.js) and added the functionality to override the duration.
const Parent = {
data() {
return {
transitionName: "slide-left",
transitionDuration: 2.5
};
},
computed: {
transitionStyle() {
return {
'transition-duration': this.transitionDuration +'s'
}
}
},
beforeRouteUpdate(to, from, next) {
const toDepth = to.path.split("/").length;
const fromDepth = from.path.split("/").length;
///////////////
// this is where you set the transition duration
// replace with your own implementation of setting duration
this.transitionDuration = Math.random()*1.5+0.5;
///////////////
this.transitionName = toDepth < fromDepth ? "slide-right" : "slide-left";
next();
},
template: `
<div class="parent">
<h2>Parent</h2>
{{transitionStyle}}
<transition :name="transitionName">
<router-view class="child-view" :style="transitionStyle"></router-view>
</transition>
</div>
`
};
This will override the duration by in-lining the style
You would still need to find a way to get the intended duration within beforeRouteUpdate, but I'll leave that up to you.
you can have a look at a working pen here: https://codepen.io/scorch/pen/LOPpYd
Note that the effect is only applied to the parent component, so animation on home(/) will not be affected