0

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>
7
  • 1
    Pagination + infinite loading + lazy-loading is the way to go. Commented Sep 17, 2024 at 7:30
  • Is there a way to prevent rerendering components when they are filtered? depends on how they are rendered in the first place Commented Sep 17, 2024 at 7:55
  • Its a v-for on Component only. Data is fetch all in. Commented Sep 17, 2024 at 8:09
  • 1
    You could use v-for+v-once and then trigger the visibility of element refs. "Is there a way to prevent rerendering components when they are filtered?" - the question is incorrect, the ones the were filtered out won't be rendered, the ones that weren't aren't re-rendered if you used the correct unique "key" as it's advised with v-for Commented Sep 17, 2024 at 8:51
  • 1
    From what you shared so far it's unclear where the bottleneck originates from: recalculating element positions, not key-ing each element properly, or rendering way more elements than can fit on the screen). I would advise sharing a minimal reproducible example to demonstrate the problem and allow testing potential fixes. Commented Sep 17, 2024 at 9:37

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.