フロントエンドの速度改善でやったこと

最近、Vue.jsで作られたWebページの速度改善をやりました。
対応内容について書き残します。

## 前置き
* ページ読み込み速度ではなく、読み込んだ後の速度を改善しました
* 対応内容は、Vue.jsに限らず、フロント全般で有効な内容です
* バックエンドの速度改善は含まれません

## 概要

| No | 何をした? | どうやった? |
| :--- | :--- | :--- |
| 1 | API呼び出しの並列化 | Promise.all()を使う |
| 2 | 大量データの一覧表示が重くならないようにする | virtual-scrollを使う |

## 対応1: API呼び出しの並列化

### **before**
```ts
async mounted() {
  // 4つのAPIを順次実行
  const response1 = await apiRequest1();
  const response2 = await apiRequest2();
  const response3 = await apiRequest3();
  const response4 = await apiRequest4();
},
```
mounted()内で複数のAPIを順次実行している箇所がありました。
当たり前の話ですが、この場合
`処理時間 = api1の実行時間 + api2の実行時間 + api3の実行時間 + api4の実行時間`
となるため、遅いです。
幸い今回はapiの実行順序を守る必要がなかったため、次のように並列化しました。

### **after**
```ts
async mounted() {
  // 4つのAPIを並列実行
  const [
    response1,
    response2,
    response3,
    response4,
  ] = await Promise.all([
    apiRequest1(),
    apiRequest2(),
    apiRequest3(),
    apiRequest4(),
  ]);
},
```
これで、
`処理時間 = api1〜4のうち一番遅いものの実行時間`
となり、速くなりました。

なお、細かいエラー処理をしたい場合は次のようにもできます。

```ts
async mounted() {
  const [
    response1,
    response2,
    response3,
    response4,
  ] = await Promise.all([
    apiRequest1().catch((err) => {
      console.log(`apiRequest1() failed: err(${err})`);
    }),
    apiRequest2().catch((err) => {
      console.log(`apiRequest2() failed: err(${err})`);
    }),
    apiRequest3().catch((err) => {
      console.log(`apiRequest3() failed: err(${err})`);
    }),
    apiRequest4().catch((err) => {
      console.log(`apiRequest4() failed: err(${err})`);
    }),
  ]);
  console.log('Promise.all finished!');
  // 例えばapiRequest4()だけ失敗した場合は、
  //   apiRequest4() failed: err(エラー内容)
  //   Promise.all finished!
  // が出る。
  // response4はundefinedになる。
},
```





## 対応2: 大量データの一覧表示が重くならないようにする

### **about**
データの一覧を表示している箇所について、データ数が多くなると、動作がもっさりする問題がありました。
対策として、virtual-scrollを導入しました。

virtual-scrollとは、ユーザーに見える部分だけDOMを生成することで、擬似的にスクロールを実現する仕組みのことを指します。
例えば1万件データがあった場合、
普通にv-forでlist表示をすると、1万個のDOMが描画されます。
一方、virtual-scrollでlist表示すると、ユーザに見える分+α(サイズにもよるけどせいぜい数十個)のDOM描画で済みます。

参考: https://kakkoyakakko2.hatenablog.com/entry/2018/10/23/003000
(ユーザに見える部分だけDOMを生成する様子がgifアニメになっており、わかりやすいです)

### **sample**
各種フレームワークにvirtual-scroll用のライブラリがあるようですが、今回はvueを使っているため、
vue-virtual-scroll-listを使いました。
以下、サンプルコードと、実行結果です。

```vue

<template>
  <div class="container">
    <h1>Qiita Test</h1>

    <!-- 普通にv-forで回すと、データ量が多いときの描画コストが大きい -->
    <h2>Before: normal list</h2>
    <ul style="height: 200px; overflow: scroll;">
      <li
        style="list-style: none;"
        v-for="user in users"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>

    <!-- virtual-scrollを使うと、表示される分だけ描写するため、描画コストがデータ量に依存しない -->
    <h2>After: virtual scroll list</h2>
    <ul style="height: 200px; overflow: scroll;">
      <vue-virtual-scroll-list
        :size="20"
        :remain="15"
      >
        <li
          v-for="user in users"
          :key="user.id"
        >
          {{ user.name }}
        </li>
      </vue-virtual-scroll-list>
    </ul>
  </div>
</template>

<script lang="ts">
import vue from 'vue';
import VueVirtualScrollList from 'vue-virtual-scroll-list';

export default vue.extend({
  components: {
    VueVirtualScrollList,
  },
  data() {
    return {
      users: [] as Array<{id: number, name: string}>,
    };
  },
  async mounted() {
    this.users = [
      {id: 1, name: 'user1'},
      {id: 2, name: 'user2'},
      {id: 3, name: 'user3'},
      {id: 4, name: 'user4'},
      // 後略。大量のユーザーがいるとします。
    ];
  },
});
</script>
```

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/7bdac83a-be2e-27de-3494-84b28de76521.png)


## 最後に

virtual-scrollは同僚に教えてもらいました。
スクロールするとDOMがゴリゴリ変わっていくのは、なかなかにお洒落。
5, 6秒かかってた処理が一瞬になったときの気持ちよさったらなかったです。