Vue.js のリストレンダリング( Virtual DOM 実装 )と相性の悪いデータの変更

この間、現場で直面したやつの個人的なメモです。対象のバージョンはこの記事を書いている時の最新バージョン v2.3.0。

以下のようなリストレンダリングをおこなうコンポーネントがあるとして、

<script>
export default {
  name: 'list',
  data() {
    return {
      items: [1, 2, 3, 4, 5]
    };
  }
}
</script>

<template>
  <ul v-for="item in items" :key="item">
    <li>{{ item }}</li>
  </ul>
</template>

items のサイズの上限を 5 として、新しいデータを追加したい場合は古いデータを削除することとする。
例えば、items [1, 2, 3, 4, 5] に 6 と 7 を追加する場合、items は [3, 4, 5, 6, 7] になるが、Vue.js の Virtual DOM 実装は以下のような diff/patch を適用する。

  • 新しい vdom の先頭の 3 が古い vdom にないか探し、見つかったので 3 のアイテムの DOM 要素をリストの先頭に移動させる(ここで key 属性の指定が効いている)。4, 5 も同様。
  • 6 は無いので、新しく DOM 要素を生成して追加する。7 も同様。
  • ここまでで 3, 4, 5, 6, 7, 1, 2 と要素が並んでいる。1 の要素は不要なので削除する。2 も同様。

以上で、新しいデータが Real DOM に反映された。このロジックは updateChildren 関数 (vue/patch.js at v2.3.0 · vuejs/vue · GitHub) に書かれている。上から下に読んでいくと分かりづらいので、上記のようなサンプルを用意し、DOM に Breakpoint を設定して実際にコードを動かしながら処理を追っていくと読みやすい。
本来であれば、1, 2 をそれぞれ変更して末尾に移動させるのが最も効率が良いはずだが、リストの最大サイズ + 変更のあった件数分 の DOM 操作が必要になる。リストの最大サイズが増えれば増えるほど、時間がかかる。

自分の場合は、最大サイズがそれなりに多かったのと、データの更新頻度が多く、Script の処理が増えて、fps がかなり低下してしまった(Timeline で調べたら、updateChildren 関数が Script 処理の 80% 以上を占めていた)。結局、この部分は直接 DOM 操作をおこなうことにした。Virtual DOM は、大体のケースである程度うまくいくが、このように極端なケースではうまくいかない。

ちなみに、フォームで何かを入力してボタンを押したらデータを1件追加する、あるいはリストから1件アイテムを削除するといったよくあるケースの場合は DOM 操作が 1 回で済むようになっている。