Maximizing Vue.js Performance: Limit Parent Component Re-rendering When Updating Children Components List

While working on a component that lists thousands of items using the v-for directive, I encountered a performance issue: updating certain items triggers the re-rendering of the parent component.

Let's consider an example: a bar chart that colors the bars around the client's cursor.

Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null
    };
  },
  template: `
<div class="bar-chart">
  <div>Rendering time for chart: {{ new Date() | time }}</div>
  <svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
    <bar
      v-for="bar in bars"
      :key="bar.id"
      :x="bar.x"
      :y="bar.y"
      :height="bar.height"
      :width="bar.width"
      :show-time="bar.showTime"
      :colored="bar.colored"
    ></bar>
  </svg>
</div>
  `,
  computed: {
    barWidth() {
      return this.width / this.data.length;
    },
    bars() {
      return this.data.map(d => {
        const x = d.id * this.barWidth;
        return {
          id: d.id,
          x: x,
          y: 160 - d.value,
          height: d.value,
          width: this.barWidth,
          showTime: this.barWidth >= 20,
          colored: this.mousePositionX &&
            x >= this.mousePositionX - this.barWidth * 3 &&
            x < this.mousePositionX + this.barWidth * 2
        }
      });
    }
  }
});

Vue.component("Bar", {
  props: ["x", "y", "width", "height", "showTime", "colored"],
  data() {
    return {
      fontSize: 14
    };
  },
  template: `
<g class="bar">
  <rect
    :x="x"
    :y="y"
    :width="width"
    :height="height"
    :fill="colored ? 'red' : 'gray'"
  ></rect>
  <text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
    {{ new Date() | time }}
  </text>
</g>
`
});

const barCount = 30; // set barCount <= 30 to display the bars with time

new Vue({
  el: "#app",
  data() {
    return {
      data: Array.from({
        length: barCount
      }, (v, i) => ({
        id: i,
        value: randomInt(80, 160)
      })),
      width: 795
    }
  }
});
body {
  margin: 0;
}

svg {
  height: 160px;
  background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  Vue.config.devtools = true;
  Vue.config.productionTip = false;
  Vue.filter("time", function(date) {
    return date.toISOString().split('T')[1].slice(0, -1)
  });

  function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
</script>
<div id="app">
  <bar-chart :data="data" :width="width" />
</div>

We can observe the re-rendering of components based on the displayed time values, which are updated only when the corresponding component is rendered.

When the color of items (Bars) is updated, only the updated items are re-rendered.
However, the problem arises when the parent component (BarChart) is also re-rendered every time the cursor moves, even if no items have changed.

For a bar chart with 30 bars, this might be acceptable.
But for a large number of bars, the extensive re-rendering of the parent component significantly impacts performance and causes delays.

Take a look at the same example with 1500 bars:

Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null
    };
  },
  template: `
<div class="bar-chart">
  <div>Rendering time for chart: {{ new Date() | time }}</div>
  <svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
    <bar
      v-for="bar in bars"
      :key="bar.id"
      :x="bar.x"
      :y="bar.y"
      :height="bar.height"
      :width="bar.width"
      :show-time="bar.showTime"
      :colored="bar.colored"
    ></bar>
  </svg>
</div>
  `,
  computed: {
    barWidth() {
      return this.width / this.data.length;
    },
    bars() {
      return this.data.map(d => {
        const x = d.id * this.barWidth;
        return {
          id: d.id,
          x: x,
          y: 160 - d.value,
          height: d.value,
          width: this.barWidth,
          showTime: this.barWidth >= 20,
          colored: this.mousePositionX &&
            x >= this.mousePositionX - this.barWidth * 3 &&
            x < this.mousePositionX + this.barWidth * 2
        }
      });
    }
  }
});

Vue.component("Bar", {
  props: ["x", "y", "width", "height", "showTime", "colored"],
  data() {
    return {
      fontSize: 14
    };
  },
  template: `
<g class="bar">
  <rect
    :x="x"
    :y="y"
    :width="width"
    :height="height"
    :fill="colored ? 'red' : 'gray'"
  ></rect>
  <text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
    {{ new Date() | time }}
  </text>
</g>
`
});

const barCount = 1500; // set barCount <= 1500 to display the bars with time

new Vue({
  el: "#app",
  data() {
    return {
      data: Array.from({
        length: barCount
      }, (v, i) => ({
        id: i,
        value: randomInt(80, 160)
      })),
      width: 795
    }
  }
});
body {
  margin: 0;
}

svg {
  height: 160px;
  background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  Vue.config.devtools = true;
  Vue.config.productionTip = false;
  Vue.filter("time", function(date) {
    return date.toISOString().split('T')[1].slice(0, -1)
  });

  function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
</script>
<div id="app">
  <bar-chart :data="data" :width="width" />
</div>

For 1500 bars, Vue Devtools clearly indicates that the time taken to re-render the parent component is too long (~278 ms), leading to performance issues.

https://i.sstatic.net/4ztNY.png

So, is there a way to update child components, which rely on parent data (such as cursor position), without causing unnecessary updates to the parent component?

Answer №1

In Vue, computed properties can be very useful but they are not always the best approach. There are some potential traps to watch out for.

One common issue is generating a new array with completely new objects every time the mouse moves. This causes the entire BarChart component to re-render, which can have performance implications especially if this process occurs frequently.

The solution lies in minimizing data changes and utilizing watchers effectively to manage updates.

Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null,
      bars: []      
    };
  },
  template: `
<div class="bar-chart">
  <div>Chart rendered: {{ new Date() | time }}</div>
  <svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
    <bar
      v-for="bar in bars"
      :key="bar.id"
      :x="bar.x"
      :y="bar.y"
      :height="bar.height"
      :width="bar.width"
      :show-time="bar.showTime"
      :colored="bar.colored"
    ></bar>
  </svg>
</div>
  `,
  computed: {
    barWidth() {
      return this.width / this.data.length;
    },
  },
  watch: {
    data: {
      handler: function() {
        this.bars = this.data.map(d => {
          const x = d.id * this.barWidth;
          return {
            id: d.id,
            x: x,
            y: 160 - d.value,
            height: d.value,
            width: this.barWidth,
            showTime: this.barWidth >= 20,
            colored: false
          }
        });
      },
      immediate: true
    },
    mousePositionX: {
      handler: 'updateBarsColor'
    }
  },
  methods: {
    updateBarsColor(x) {
      this.bars.forEach(bar => {
        bar.colored = x &&
          bar.x >= x - this.barWidth * 3 &&
          bar.x < x + this.barWidth * 2
      })
    }
  }
});

Vue.component("Bar", {
  props: ["x", "y", "width", "height", "showTime", "colored"],
  data() {
    return {
      fontSize: 14
    };
  },
  template: `
<g class="bar">
  <rect
    :x="x"
    :y="y"
    :width="width"
    :height="height"
    :fill="colored ? 'red' : 'gray'"
  ></rect>
  <text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
    {{ new Date() | time }}
  </text>
</g>
`
});

const barCount = 1500; // to display the bars time, set barCount <= 30

new Vue({
  el: "#app",
  data() {
    return {
      data: Array.from({
        length: barCount
      }, (v, i) => ({
        id: i,
        value: randomInt(80, 160)
      })),
      width: 795
    }
  }
});
body {
  margin: 0;
}

svg {
  height: 160px;
  background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  Vue.config.devtools = true;
  Vue.config.productionTip = false;
  Vue.filter("time", function(date) {
    return date.toISOString().split('T')[1].slice(0, -1)
  });

  function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
</script>
<div id="app">
  <bar-chart :data="data" :width="width" />
</div>

UPDATE: In response to a question raised in the comments

It's working fine, but I noticed that the BarChart still re-renders (you can see the time changing). Is this causing any performance issues?

After analyzing the situation, it seems that the constant re-rendering of the BarChart component is due to how the child components are being passed props. When each property of the config object is deconstructed and passed separately to the Bar component, any change in these properties triggers a re-render of the parent component.

To mitigate this issue, consider passing the entire object as a prop to the Bar component. This approach eliminates unnecessary re-renders and improves performance, as demonstrated in the updated example provided.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Setting up GoogleProvider with Next Auth in your Next 13 application: A step-by-step guide

Having some trouble setting up the authentication system for my NextJS 13 Experimental App and src folder. Every time I try to authenticate, I get redirected to http://localhost:3000/api/auth/error with a 404 error. I have configured Google OAuth Credenti ...

"Upgrade your website visuals by dynamically changing image sources using jQuery and adding a touch of flair with smooth loading

I am currently working on a page where I have an image that changes its source using JavaScript. However, I would like to enhance this by adding a beautiful sliding effect when the image changes. My goal is to create a seamless transition so that when a ...

Drag and drop a Jquery image onto the div with its top left corner

Drop targets: <div id="targetContainer" style="width:100px; height:100px;> <div id="tar_0-0" class="target" style="position: relative; float:left; width:50px; height:50px; background-color: #fff;"></div> <div id="tar_1-0" class="t ...

What is the optimal method for navigating through a complex nested object in Angular?

Looking to navigate through a nested object structure called purchase. Within this structure, there is a sub-array named purchaseProducts which contains another sub-array called products along with additional data. What methods do you suggest for efficien ...

What is the proper way to end a WebSocket connection?

Can a websocket connection be terminated by the server without closing the entire server? If so, how can this be done? Note: My back-end is implemented using NodeJS and I am using the 'ws' websocket module. ...

Each loop in the forEach function triggers the mouseup event

I am currently utilizing a foreach loop: objects.forEach(function(object) { var button = '<tr><td>' + object.object.code + '</td><td>' + formatDistance(1456000) + &apos ...

How can I display a date as dd/mm/yyyy in a datatable column using Javascript or Jquery?

Sorting a date column in a datatable can be tricky, especially when it is formatted as dd/mm/yyyy. The issue arises when the column sorts the dates as strings rather than considering the month. This results in incorrect sorting where the day becomes the pr ...

Issues with recursive functions in Javascript

I'm currently facing a challenge with javascript recursion. Below is the snippet of code that I am having trouble with: _parseJson: function($obj, $json_arr) { for (i = 0; i < $json_arr.length; i++) { var $el = document_creator.create ...

Using the debug module, we're able to set up debugging for our Express application with the specific tag "express-locallibrary-tutorial:server". I am curious to understand the purpose and significance of this setup

I've been diving into backend development with Express lately. I decided to work on the express-locallibrary-tutorial project from GitHub. However, I'm having trouble grasping something. var debug = require('debug')('express-locall ...

Jest mocking function that retrieves or sets a value

Looking to run a test on a component that utilizes the following mixin method: methods: { getActiveTab() { return new Promise((resolve) => chrome.tabs.query({ active: true }, tabs => resolve(tabs[0]))); }, ... Attempting to mock this m ...

The online server is unable to access the route from the ajax function in a separate JavaScript file

I am currently working on a Laravel project where each view page has its own separate JS file. However, I have encountered an issue when trying to access route functions from AJAX post or get calls on the online server (Digital Ocean). The error message I ...

Creating an overlay with CSS hover on a separate element and the ::before pseudo class

Issue: I am struggling to display the overlay when hovering over the circle element, rather than just the image itself. I have attempted to achieve this using CSS, but can't seem to make it work as intended. Any guidance and examples using JavaScript ...

Having trouble with Cordova, Angular, and PushPlugin? Your callback isn't firing

While searching for solutions, I noticed various similar items but none were quite the same context as mine (Angular specifically, not using Ionic). My Cordova version is 5.0.0 (confirmed through 'cordova --version' in CMD showing 5.0.0, and in t ...

Discovering elements that are shallower than a chosen selector

I'm currently developing a jQuery plugin designed to facilitate the management of form collections. This plugin is intended to incorporate buttons for adding, removing, moving up, and moving down within the collection. In every collection's r ...

Swap out the svg element with an icon alternative

Transforming Circle Elements into Shopping Cart Icons I am attempting to change the appearance of my SVG circle elements to resemble shopping carts. Is there a method to completely alter the definition of a circle element in svg so that it displays a spec ...

Tips and tricks for manipulating base64 images in Node.js

I have a unique challenge - I want to manipulate a base64 picture by adding just one extra pixel. My goal is to send a base64 image string (e.g. data:image/png;base64,iVBORw0KG...) from my express server. Let's say the image is 100x100px and I need to ...

Sharing functions between Angular components

Check out my problem statement: https://stackblitz.com/edit/angular-jk8dsj I'm facing two challenges with this assignment: I need to dynamically add elements in the app.component when clicking a button in the key-value.component. I've tried ...

Synchronize custom directive controller data with data from a factory

As a newcomer to angularjs, I stumbled upon this example that I found somewhere and it works perfectly well. However, I am puzzled by how the data within the customized directive controller remains synchronized with the factory data. Here is the snippet of ...

Angularfire2: Access Denied Error When User Logs Out

When utilizing the following method: login() { this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider()) .then(() => { this.router.navigate(['']); }); } An error occurs during logout: zone.js:915 Unca ...

Attempting to simultaneously start my node.js server and React .js app, resulting in the error message 'ERR_MODULE_NOT_FOUND'

Currently facing issues while setting up a server for an ecommerce site being built in React. Attempting to run both the React app and the server simultaneously using npm run dev, but encountering various errors that persist even after attempted fixes foun ...