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?