I have a problem with optimalization in vue.js. I know my thinking is not perfect but i need to optimize part of application quick.
I have app looking like Pinterest. I have 1k cards to show, every card have profile data some of them have more some of them have less. Problem is when i try to filter them i have to wait a lot of time to have them filtered. All cards are fetch by ajax all at once.
What i found is Vue.js is rerendering them every time i filter them. They are static data witch wont change and it cant be changed.
Is there a way to prevent rerendering components when they are filtered?
My example code: Template:
<template>
<div>
<div class="results-wrapper" v-if="loaded" v-show="mode == 'results'">
<div class="side-wrapper" :class="{ show: mobileFilterState }">
<Filters :filters="structuredFilters" @closeFilters="mobileFilterState = false; $functions.hideBackdrop()"/>
</div>
<div class="content-wrapper">
<input type="hidden" name="terms" :value="mainFilters.map((option) => option.name).join(', ')">
<div v-if="!debugMode">
<div>
<SearchResult :counter="filteredProfiles.length" :filters="mainFilters" :modifier="modifier" @switchModifier="switchModifier" @removeFilter="removeFilter" @openSearcher="mode = 'searcher'"/>
</div>
<div>
<ActiveFilters :filters="flattenFilters" />
</div>
<ProfileList :profiles="filteredProfiles"/>
</div>
<div v-else>
<ProfileDebug :profiles="filteredProfiles" :selected-filters="selectedFilters" :filters="flattenFilters"/>
</div>
</div>
</div>
<div v-show="mode == 'searcher'" style="position: relative;">
<SearcherBackdrop :main-filters="mainFilters" @newSearch="newSearch" />
<CloseModalButton v-if="mainFilters.length" @click="mode = 'results'" style="position: absolute; top: 15px; right: 15px;"/>
</div>
<div v-show="mode == 'error'">
{{ $trans('web_search.checkout_response_messages.simple_something_went_wrong') }}
</div>
<CandidateCart v-show="mobileFilterState == false" :profiles="profiles" @show="$refs.CheckoutProcessRef.showModal('StartModal')"/>
<CheckoutProcess ref="CheckoutProcessRef"/>
</div>
<MobileFooter v-show="mode != 'searcher' && mobileFilterState == false" @openFilters="mobileFilterState = true; $functions.showBackdrop()" @openSearcher="mode = 'searcher'"/>
Profile list have:
<template>
<div class="wrapper">
<ProfileCard v-for="(profile, index) in profiles" :profile="profile" :key="index" ref="items" :data-profile-id="profile.id">
</ProfileCard>
</div>
</template>
<script>
import Collapse from './Utils/Collapse';
import ProfileCard from './Profile/ProfileCard';
import ProfileCardButton from './Profile/ProfileCardButton';
import ProfileCardHeader from './Profile/ProfileCardHeader';
import ProfileCardScores from './Profile/ProfileCardScores';
import ProfileJobCompetenceScoring from './Profile/ProfileJobCompetenceScoring';
import ProfileIndustryCompetenceScoring from './Profile/ProfileIndustryCompetenceScoring';
import ProfilePublications from './Profile/ProfilePublications';
import ProfileQualifications from './Profile/ProfileQualifications';
export default {
components: {
Collapse,
ProfileCard,
ProfileCardButton,
ProfileCardHeader,
ProfileCardScores,
ProfileJobCompetenceScoring,
ProfileIndustryCompetenceScoring,
ProfilePublications,
ProfileQualifications,
},
props: {
profiles: {
type: Array,
required: false,
},
},
}
</script>
And logic of fetching data and filtering.
export default {
components: {
ActiveFilters,
CandidateCart,
CheckoutProcess,
CloseModalButton,
Filters,
ProfileList,
ProfileDebug,
SearcherBackdrop,
SearchResult,
MobileFooter,
},
data() {
return {
loaded: false,
profiles: [],
mainFilters: [],
selectedFilters: {},
debugMode: false,
modifier: undefined,
mode: undefined,
mobileFilterState: false,
};
},
created() {
const url = new URL(window.location.href);
this.fetchData(url);
},
mounted() {
const url = new URL(window.location.href);
this.debugMode = url.searchParams.has('debug') ? !!url.searchParams.get('debug') : false;
},
methods: {
switchModifier(modifier) {
let url = new URL(window.location.href);
url.searchParams.set('modifier', modifier);
this.fetchData(url);
},
newSearch(data) {
this.mode = undefined;
let url = new URL(window.location.href);
url.searchParams.set('competence', data.competence);
url.searchParams.set('job_tag', data.job_tag);
url.searchParams.set('modifier', data.modifier);
this.fetchData(url);
},
removeFilter(filter) {
let url = new URL(window.location.href);
if (filter.type == 'jobtag') {
filter.type = 'job_tag';
}
let options = url.searchParams.get(filter.type);
options = options.split('-').filter(function (optionId) {
return parseInt(optionId) !== filter.id;
});
url.searchParams.set(filter.type, options.join('-'));
this.fetchData(url);
},
fetchData(url) {
this.loaded = false;
this.$functions.showLoader();
history.pushState({}, "", url);
url.searchParams.set('format', 'json');
this.FilterOptionsStore.clearAllOptions();
this.profiles = [];
this.SelectedFiltersStore.registerFilters(Object.keys(this.flattenFilters));
fetch(url.href)
.then((res) => res.json())
.then((json) => {
this.loaded = true;
this.$functions.hideLoader();
this.FilterOptionsStore.setProfiles(json.profiles);
this.FilterOptionsStore.setOptions('availabilityStatus', json.filters.availabilityStatus);
this.FilterOptionsStore.setOptions('competencies', json.filters.competencies);
this.FilterOptionsStore.setOptions('industrySectors', json.filters.industrySectors);
this.FilterOptionsStore.setOptions('regions', json.filters.regions);
this.FilterOptionsStore.setOptions('companySizes', json.filters.companySizes);
this.FilterOptionsStore.setOptions('educationLevels', json.filters.educationLevels);
this.FilterOptionsStore.setOptions('languages', json.filters.languages);
this.profiles = json.profiles;
this.mainFilters = json.main_filters;
this.modifier = json.modifier;
this.SelectedFiltersStore.registerFilters(Object.keys(this.flattenFilters));
if (this.mainFilters.length) {
this.mode = 'results';
} else {
this.mode = 'searcher';
}
})
.catch((error) => {
console.log(error);
this.mode = 'error';
this.$functions.hideLoader();
})
.finally(() => {
});
},
},
computed: {
...mapStores(useFilterOptionsStore), // useFilterOptionsStore => this.FilterOptionsStore
...mapStores(useSelectedFiltersStore), // useSelectedFiltersStore => this.SelectedFiltersStore
structuredFilters() {
let filters = this.FilterOptionsStore.getAllOptions();
let structuredFilters = {
competencies: {
title: this.$trans('web_search.filter_names.qualifications'),
options: filters.competencies,
searchbar: true,
modifierSwitch: true,
hidden: (filters.competencies.length == 0),
},
industrySectors: {
title: this.$trans('web_search.filter_names.industry_sectors'),
options: filters.industrySectors,
hidden: (filters.industrySectors.length == 0),
},
availabilityStatus: {
title: this.$trans('web_search.filter_names.availability'),
options: filters.availabilityStatus,
modifierState: 'OR',
hidden: (filters.availabilityStatus.length == 0),
},
regions: {
title: this.$trans('web_search.filter_names.regions'),
filters: filters.regions,
hidden: (filters.regions.length == 0),
},
companySizes: {
title: this.$trans('web_search.filter_names.company_size'),
options: filters.companySizes,
hidden: (filters.companySizes.length == 0),
maxLength: -1,
},
educationLevels: {
title: this.$trans('web_search.filter_names.education'),
filters: filters.educationLevels,
hidden: (filters.educationLevels.length == 0),
},
languages: {
title: this.$trans('web_search.filter_names.languages'),
filters: filters.languages,
hidden: (filters.languages.length == 0),
},
};
return structuredFilters;
},
flattenFilters() {
return this.FilterOptionsStore.getAllFlattenOptions();
},
filteredProfiles() {
let activeFilters = this.SelectedFiltersStore.getActiveFilters();
let modifiers = this.SelectedFiltersStore.getModifiers();
if(this.SelectedFiltersStore.getActiveFiltersCount() == 0) {
return this.profiles;
}
let tmpProfiles = this.profiles;
return tmpProfiles
.filter((profile) => {
if (activeFilters.availabilityStatus.length == 0) {
return true;
}
if (Object.hasOwn(modifiers, 'availabilityStatus') && modifiers.availabilityStatus == 'OR') {
return activeFilters.availabilityStatus.some((optionId) => {
return profile.availability_status == optionId;
});
}
return activeFilters.availabilityStatus.every((optionId) => {
return profile.availability_status == optionId;
});
})
.filter((profile) => {
if (activeFilters.competencies.length == 0) {
return true;
}
if (Object.hasOwn(modifiers, 'competencies') && modifiers.competencies == 'OR') {
return activeFilters.competencies.some((optionId) => {
return profile.filter_competencies.includes(parseInt(optionId));
});
}
return activeFilters.competencies.every((optionId) => {
return profile.filter_competencies.includes(parseInt(optionId));
});
})
.filter((profile) => {
if (activeFilters.industrySectors.length == 0) {
return true;
}
if (Object.hasOwn(modifiers, 'industrySectors') && modifiers.industrySectors == 'OR') {
return activeFilters.industrySectors.some((optionId) => {
return profile.filter_industry_sectors.includes(parseInt(optionId));
});
}
return activeFilters.industrySectors.every((optionId) => {
return profile.filter_industry_sectors.includes(parseInt(optionId));
});
})
.filter((profile) => {
if (activeFilters.regions.length == 0) {
return true;
}
if (Object.hasOwn(modifiers, 'regions') && modifiers.regions == 'OR') {
return activeFilters.regions.some((optionId) => {
return profile.filter_regions.includes(optionId);
});
}
return activeFilters.regions.every((optionId) => {
return profile.filter_regions.includes(optionId);
});
})
.filter((profile) => {
if (activeFilters.languages.length == 0) {
return true;
}
if (Object.hasOwn(modifiers, 'languages') && modifiers.languages == 'OR') {
return activeFilters.languages.some((optionId) => {
return profile.filter_languages.includes(parseInt(optionId));
});
}
return activeFilters.languages.every((optionId) => {
return profile.filter_languages.includes(optionId);
});
})
.filter((profile) => {
if (activeFilters.educationLevels.length == 0) {
return true;
}
if (Object.hasOwn(modifiers, 'educationLevels') && modifiers.educationLevels == 'OR') {
return activeFilters.educationLevels.some((optionId) => {
return profile.filter_education.includes(parseInt(optionId));
});
}
return activeFilters.educationLevels.every((optionId) => {
return profile.filter_education.includes(optionId);
});
})
.filter((profile) => {
if (activeFilters.companySizes.length == 0) {
return true;
}
if (Object.hasOwn(modifiers, 'companySizes') && modifiers.companySizes == 'OR') {
return activeFilters.companySizes.some((optionId) => {
return profile.filter_company_sizes.includes(parseInt(optionId));
});
}
return activeFilters.companySizes.every((optionId) => {
return profile.filter_company_sizes.includes(optionId);
});
});
},
},
}
</script>
Also profile card have its own components to display card properly
<template>
<div class="profile-card">
<ProfileCardHeader
:availability="profile.availability_status"
:avatar-url="profile.avatar_url"
:score="profile.percentage_score_avg"
/>
<ProfileCardScores :scores="profile.scores" />
<ProfileCardButton :profile="profile" />
<Collapse
v-show="profile.content_job_scorings.length"
:title="$trans('web_search.profile_card.relevant_professional_experience')"
>
<ProfileJobCompetenceScoring :data="profile.content_job_scorings"/>
</Collapse>
<Collapse
v-show="profile.content_industry_scorings.length"
:title="$trans('web_search.profile_card.relevant_industry_experience')"
>
<ProfileIndustryCompetenceScoring :data="profile.content_industry_scorings"/>
</Collapse>
<Collapse
v-show="profile.content_qualifications.length"
:title="$trans('web_search.profile_card.qualifications')"
>
<ProfileQualifications :data="profile.content_qualifications"/>
</Collapse>
<Collapse
v-show="profile.content_publications.length"
:title="$trans('web_search.profile_card.publications')"
>
<ProfilePublications :data="profile.content_publications"/>
</Collapse>
</div>
Is there a way to prevent rerendering components when they are filtered?depends on how they are rendered in the first place