Skip to content
wannacode

Speed: when a component renders again

In short: In Vue the component that reads a changed value renders again, and a child does when its props are new; you measure first, then computed keeps the same result.

Problem

Every letter renders the whole list

At the top there is a search field and a "Search" button. The list is filtered only after "Search". ProductList writes to the console every time it renders. Type "milk" in the field and watch the console.

Vue

Four letters, four new "rendering ProductList" lines. The list is the same: you have not pressed "Search" yet. Here there are eight products and you do not feel it. In a real catalogue there are 2000, each with an image and a price, and the field starts to lag behind your fingers.

The idea

When a component renders again

A component renders again when the data it shows change. But what "its data" means, the two libraries decide differently. Both give the right result; the difference is how much work they do.

Vue tracks which reactive values each component reads while it renders. When draft changes, Vue renders again only the components that read draft: here App. A child renders again if its props are different from the previous ones (it compares them one by one).

The counter in ProductList.vue is in onBeforeMount and onBeforeUpdate: Vue calls them before the first and before every next render of the component.

Here the template passes :products="visible()": a function that filters. It runs on every render of App and returns a new array. For Vue that is a new prop, so it renders ProductList too.

Measure first, fix after. A counter in the console, like here, is the simplest measure. Vue DevTools have a Timeline with the renders of the components and their times. You fix only a render that is both extra and slow.

See it

See who lights up

<script setup>
const draft = ref('');
const query = ref('');
const visible = computed(() => filterByName(PRODUCTS, query.value));
</script>
<template>
<input v-model="draft" />
<button @click="query = draft">Search</button>
<ProductList :products="visible" @add="add" />
</template>
<!-- ProductList.vue -->
<script setup>
defineProps(['products']);
onBeforeUpdate(() => console.log('rendering ProductList'));
</script>
Step by step, in words
  1. App keeps the text of the field (draft) and the search (query). ProductList draws the cards. The console counts how many times the list renders.
  2. Before the fix the template called a function, visible(). Every render of App made a new array, so Vue saw a new prop and rendered ProductList again.
  3. computed keeps the same array while query is the same. Vue tracks what each component reads and compares the props of the child.
  4. Now a key renders only App and the field. The props of ProductList are the same, so it is skipped, and the counter stays.
  5. "Search" changes query. The array is new, so ProductList renders. This render is needed: the list shows two products now.
  6. Measure first: a counter in the console or the Profiler in DevTools. Fix only the renders that are both extra and slow.
Vue does not render the child if its props are the same. So what you pass matters: computed keeps the array, while a function in the template gives a new one on every render.

Try it

Only "Search" renders the list

Make typing in the field and "Add" not render ProductList again. Only "Search" should render it, because only then does the list change. The check types "milk", adds two products, presses "Search" and counts the "rendering ProductList" lines in the console.

Vue

Try it

Find the bug

visible is computed, and the list still renders on every letter. Which line is to blame?

Pick the line with the bug.

Try it

Fill in

How does the expensive list stay the same between renders?

const visible = (
  () => PRODUCTS.filter((p) => p.name.includes(query.value)),
);

The idea

A long list: render less

The fastest element is the one that is not drawn. A person sees 10–20 products on the screen, not 2000. So a long list draws a part:

  • "Show more": the first 20, then 20 more on a click. Only how many are shown is remembered.
  • Pages: "1, 2, 3 …", often with the number in the address (/?page=2), so it can be shared.
  • A virtual list (windowing): draws only the rows that are visible and swaps them while scrolling. It is hard to write, so you use a library: TanStack Virtual or vue-virtual-scroller.

The keys from the lesson on lists help here too: with key={p.id} (:key="p.id") the 20 added products are new elements, and the old ones stay without being made again.

The idea

Lazy loading

Not every part of the page is needed at once. The product reviews, the map of the shops, the profile editor: they can load only when the person opens them. Then the first load downloads less code.

import { defineAsyncComponent } from 'vue';
const Reviews = defineAsyncComponent(() => import('./Reviews.vue'));

import('./Reviews.vue') is a function call that loads the file and returns a Promise. defineAsyncComponent turns it into a component that is used like any other: <Reviews v-if="open" />.

Vue

Open the console: the line from reviews.js comes only after the click. To be honest, in the sandbox all files are already in the page, so here import() only runs the file later. In a real project Vite puts each such file apart and the browser downloads it from the server on the first click. The same goes for pages: in Vue Router component: () => import('./CartPage.vue') loads the page on the first move to it.

More precisely, if you are curious

v-memo. For a very long v-for Vue has v-memo="[p.id === selected]": the row renders again only if the values in the array change. The docs call it a micro-optimisation for rare cases. A wrong array means a missed update.

State down. The field with its draft can be a separate component SearchBox that emits search up only on "Search". Then the letters change only SearchBox.

computed with a stable result. From Vue 3.4 on, a computed that returns the same primitive value (say true) does not trigger what depends on it. For an object you can return the previous one (computed((old) => …)) if the content is the same.

Development mode is slower. Warnings and checks cost time. Measure the speed of the built version (vite build and vite preview), not only in development.

Exercise

Show more

The catalogue has 10 products and draws them all. Show the first 4, and below them "Showing 4 of 10" and a "Show more" button that adds 4 more. When all are shown, the button goes away. The check counts the products after every click and looks at the text and the button.

Vue

Test

Check yourself

1. The product page is slow. What is the first thing you do?
2. App passes :products="visible()", where visible is a function that filters. You type in the field of App. Does ProductList render?
3. The catalogue has 5000 products and the page opens slowly. What helps most?
4. What does lazy loading a component (lazy / defineAsyncComponent) give in a real project?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·