【问题标题】:Vue.js performance: avoid parent component re-rendering when the list of children components changesVue.js 性能:避免子组件列表更改时父组件重新渲染
【发布时间】:2021-07-11 20:07:43
【问题描述】:

使用 v-for 指令处理列出数千个项目的组件时,我遇到了性能问题:更新某些项目会导致父组件重新渲染。

我们可以举个例子:一个条形图,为客户光标周围的条形着色

Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null
    };
  },
  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;
    },
    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; // 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>

我们可以看到组件重新渲染,这要归功于显示的时间值,只有在相应的组件被渲染时才会更新。

更新项目(条形图)颜色时,仅重新渲染更新的项目。
但是,这就是问题,即使没有更改任何项目,父级 (BarChart) 也会在每次光标移动时重新渲染。

对于具有 30 个条形的条形图,它可能没问题。
但是如果显示大量条形图,重新渲染父组件所花费的时间太大,会导致严重的性能损失。

看看同样的例子有 1500 个柱:

Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null
    };
  },
  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;
    },
    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; // 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>

对于 1500 条,Vue Devtools 清楚地表明重新渲染父组件所花费的时间太长(约 278 毫秒)并导致性能问题。

那么,有没有办法更新子组件,这取决于父组件的数据(如光标位置),避免父组件不必要的更新?

【问题讨论】:

    标签: javascript performance vue.js svg vuejs2


    【解决方案1】:

    计算属性在 Vue 中非常有用……但并非总是如此。还有一些陷阱……

    每次鼠标移动时使用一组全新的对象生成新数组就是其中之一。因为新数组整个 BarChart 组件必须重新渲染(并且每 0.X 秒的新数组也不是免费的)。

    解决方案是尽量减少数据更改......在这种情况下使用观察者。

    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>

    更新 - 附加问题(来自 cmets)

    好的,它有效。但令我惊讶的是 BarChart 仍然重新渲染(你会看到时间在变化)。这不会给性能带来麻烦吗?

    经过一番思考,我得出一个结论,BarChart 组件每次似乎无缘无故地重新渲染的原因是因为组件将道具传递给Bar 孩子的方式。在您的原始(和我的第一个)示例中,BarChart 是将 bar 配置对象“解构”为单独的道具。这样,BarChart 组件依赖于配置对象的每个属性,并且每次更改数组中任何对象的任何属性时都需要重新渲染(以更新子道具)

    解决这个问题的方法是将整个对象传递给Bar 组件。请参阅我的第二个示例,它更快(BarChart 根本不重新渲染)

    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"
          :config="bar"
        ></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: ["config"],
      data() {
        return {
          fontSize: 14
        };
      },
      template: `
    <g class="bar">
      <rect
        :x="config.x"
        :y="config.y"
        :width="config.width"
        :height="config.height"
        :fill="config.colored ? 'red' : 'gray'"
      ></rect>
      <text v-if="config.showTime" :transform="'translate(' + (config.x + config.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>

    【讨论】:

    • 你当然可以通过传递 prev 值而不是 forEach 来编写更有效的 updateBarsColor 函数,但我会把这部分留给你......
    • 好的,它有效。但我很惊讶BarChart 仍然重新渲染(你会看到时间在变化)。这不会给性能带来麻烦吗?我们能否得出结论,问题只是我们每 10 毫秒构建了一个新的大数组?
    • 我并不是说我是 Vue 内部的专家。我不确定它为什么起作用...Bar 组件道具只是原始值,而不是对象,对吗?无论如何,你会惊讶于分配和 make reactive 一个包含 1500 个对象的新数组需要多少时间...
    猜你喜欢
    • 2022-06-29
    • 2020-05-04
    • 1970-01-01
    • 2018-08-12
    • 2019-06-01
    • 2018-12-24
    • 2021-03-12
    • 1970-01-01
    • 2021-06-05
    相关资源
    最近更新 更多