あまり頭を使わずVue + Vuetify + TypeScriptの初期描画を高速化するためにやったこと

## やりたいこと
Vue + Vuetify + TypeScriptで書かれたWebアプリを**はじめて開いたときの描画を高速化**したいです。初期描画が早いと第一印象が良く速いほどと嬉しいです。ロジック部分を高速化できそうなアルゴリズム書き換えたりせず、なるべく単純作業だけで高速化できそうなことをまとめます。

具体的には「[マルチデバイス間でファイル転送を手軽にしたい！スマホでもPCでもcurlでも - Qiita](https://qiita.com/nwtgck/items/2dd47031ba729256f5eb)」のWebアプリを高速化します。もともと、デスクトップ環境だとそれほど初期描画の速度は気にならないのでモバイルでの初期描画の比較を以下に載せます。


## 高速化の前と後での比較

最終的にどれぐらいの差が出たかの比較動画です。
**左が高速化前で、右が高速化後**です。
![piping-ui-before-after-dynamic.mp4.opt.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/f10006ff-3f30-3633-f959-de16df481691.gif)
画質が良い動画: <https://youtu.be/I9VTpnpB6OM>

体感でも描画までにかかる速度の時間がわかります。
iOS (iPadOS)のGoogle Chromeで、キャッシュなどを排除するためすべてのタブを消したシークレットモードで各ページを開いて録画しました。
（PWAのおかげで1度開けば、モバイルでも元々そこまで初期描画に時間はかかりませんでした。）

ブランチデプロイ出来るNetlifyを生かして高速化の前と後のPageSpeed Insghtsのスコアを比較します。

### 高速化前
<https://before-dynamic-import--piping-ui.netlify.com/>
<img width="650" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/6e39c6cb-f8f6-47b6-7d82-498b5977f825.png">


### 高速化後
<https://after-dynamic-import--piping-ui.netlify.com/>
<img width="650" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/6e777d8e-3615-b6a0-0c7b-e9a225cd38d7.png">

<!-- <img width="650" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/39f5abb1-8501-819d-6bce-dc322f26c3eb.png"> -->

PageSpeed Insightsのスコアは多少変動あって同じサイトでも70台とか89とかのときもありました。


元々、dynamic importも非同期コンポーネントも一切使っていなかったのでそれらを使うと高速化したという話です。それに加えてフォント/アイコン関連も重かったのでそれらのインポートを変更したことについて書きます。

## 非同期コンポーネント

単純に以下のように`import .. from ...`を`const ... = () => import(...)`のように書き換えます。

```ts
// 同期（普通）
import PipingUI from '@/components/PipingUI.vue';

// ==> 

// 非同期
const PipingUI = () => import('@/components/PipingUI.vue');
```

`@Components({components: {...}})`の部分を変える必要などはありませんでした。描画に必要のない(`v-if`で表示されないなど)コンポーネントはロードされないため、そのコンポーネントに関するJavaScriptやCSSが実行を初期にしなくてすみ高速化出来ます。

Vue公式: [動的 & 非同期コンポーネント — Vue.js](https://jp.vuejs.org/v2/guide/components-dynamic-async.html)

## コンポーネント化する

コンポーネントとして切り出して上記の非同期コンポーネントとしてimportすると初期描画が高速化することがあります。コンポーネント複数回使ったり再利用しなくても役割でいくつか切り出すと速度に差が出ました。

具体的には、今回高速化対象のWebアプリはOSSライセンスを一覧する機能があって、そのライセンス情報をJSONと保持しています。コンポーネント化して、非同期コンポーネントとしてimportすると、特定のボタンが押されるまでライセンスのJSONロードなどが遅延され、初期描画が早くなりました。

<!-- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/f7459c63-3519-45bf-9b8e-17002ea39bce.png) -->
<img width="650" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/f7459c63-3519-45bf-9b8e-17002ea39bce.png">


## Dynamic importする

以下のようにして書きなおしました。

```ts
// 同期（普通）
import * as FileSaver from 'file-saver';

// ==>

// 非同期
const FileSaverAsync = () => import('file-saver');
```

以下のように、使うときは`await FileSaverAsync();`でライブラリがロード出来ます。`async-await`の文脈で行います。

```ts
(async () => {
  // ロード
  const FileSaver = await FileSaverAsync();
  FileSaver.saveAs(myBlob, fileName);
})();
```


`* as ...`のようにしていないdefaultを読み込んでいる場合は以下のように書き直しました。

```ts
import JSZip from "jszip";

// ==>

const JSZipAsync = () => import('jszip').then(p => p.default);
```

Dynamic importするとasync-awaitが必要になるため、async-awaitな算出プロパティが欲しくなることがあります。その時は以下の方法を使います。

### Dynamic import - 非同期な算出プロパティが欲しい

[vue-async-computed](https://github.com/foxbenjaminfox/vue-async-computed)というライブラリを使います。
TypeScriptでこのライブラリを使う方法は以前に以下の記事でまとめて、今回もこの方法を使いました。
「[Vue + TypeScriptで非同期な算出プロパティを使いたい！ - Promiseとかasync-await - Qiita](https://qiita.com/nwtgck/items/5050dea582813a68f46d)」

ただし、Vuetify 2.xになってからTypeScriptとvue-async-computedの組み合わせで上記の記事通りやってもコンパイルエラーします(1.5だと大丈夫です)。
そのため、上記の記事で`types/vue-async-computed.d.ts`を作成するステップは飛ばして、以下のように`main.ts`で`@ts-ignore`をつけて無理やり`vue-async-computed`をインポートします。

```main.ts
// @ts-ignore
import AsyncComputed from 'vue-async-computed';
```

対応するコミットはこれです: <https://github.com/nwtgck/piping-ui-web/commit/0f5874e8658e634b6ab8b382dbc5fcaba4e36332#diff-8cfead41d88ad47d44509a8ab0a109ad>


### マテリアルデザインのアイコンで`@mdi/js`を使う

お手軽な方法は`npm i -D @mdi/font`でインストールして、`import '@mdi/font/css/materialdesignicons.css`する方法です。こうすれば`<v-icon>mdi-account</v-icon>`と書いたものが自動で`mid-account`に対応するアイコンになってくれます。

ですが、これだと使ってないアイコンもbundleされてサイズが増えて初期描画時の負担になります。そこで`@mdi/js`を使います。以下のようにインストールします。

```bash:インストール
npm install -D @mdi/js
```

Vuetify 2以降の場合は、`src/plugins/vuetify.ts`を以下のようにします。

```src/plugins/vuetify.ts
import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: 'mdiSvg',
  },
})
```

以下のように書くことで、必要に応じてアイコンをインポートできるようです。変数`mdiAccount`は`"mdi-account"`が入っています。

```ts
import { mdiAccount } from '@mdi/js'
```

変更のコミットです: <https://github.com/nwtgck/piping-ui-web/commit/bf0d67279d253a8c5d07d9014ea78769279e2ce4>。変更の参考になるかも知れません。

Vuetify公式: [Icons — Vuetify.js](https://vuetifyjs.com/en/customization/icons#install-material-design-icons-js-svg)

### Font Awesomeのアイコンを最適化する

最適化前は以下のようにインポートしてました。

```src/main.ts
// 最適化前
import '@fortawesome/fontawesome';
import '@fortawesome/fontawesome-free-brands';
```

ただ、今回のプロジェクトではGitHubのアイコンだけが欲しかっただけで上記だと不要なものがたくさんありました。そこで、以下のvue-fontawesomeのREADME.mdに書かれている方法を使って必要なアイコンだけのインポートに変更しました。

Fort Awesome公式: [FortAwesome/vue-fontawesome at 700a86cb1a3726364de7137d0cbee2e00fcfd30d](https://github.com/FortAwesome/vue-fontawesome/tree/700a86cb1a3726364de7137d0cbee2e00fcfd30d#usage)

```src/main.ts
import { library } from '@fortawesome/fontawesome-svg-core'
// 好きなアイコンだけインポート（今回はGitHubロゴだけ）
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'


library.add(faGithub);
Vue.component('font-awesome-icon', FontAwesomeIcon);
```

Vueの`<template>`内では`<font-awesome-icon :icon="['fab', 'github']" />`のようにして使います。

変更のコミットです: <https://github.com/nwtgck/piping-ui-web/commit/e05fee06486e35f805774b9f917e789331b13cb5>。

## CSSの非同期インポート

TODO: レンダリングブロックすると思って非同期でインポートしましたが、効果があるかは要調査です（bundle後の自動生成のコードを未確認）。


```ts
import 'filepond/dist/filepond.min.css';

// ==>

(async () => require('filepond/dist/filepond.min.css'))();
```

PageSpeed Insights上で「上記のCSSでpreload使ったら早くなるよ」みたいな警告出てたのが消えたので効果があったかもしれないです。

## おまけ: ビルドされたファイルの差異

以下のように、高速化前はDynamic importや非同期コンポーネントも使っていなくCode splittingされずに、chunk-vendorsがJS,CSS共にとても大きいです。これをパースして評価しないと初期描画出来ないはずなので、初期描画に少し時間がかかっていたのだと思います。

```:高速化前
  File                                      Size             Gzipped

  dist/js/chunk-vendors.27ddbf66.js         1708.28 KiB      585.92 KiB
  dist/js/app.7478db47.js                   64.12 KiB        15.10 KiB
  dist/service-worker.js                    1.98 KiB         1.01 KiB
  dist/precache-manifest.bbb4bb2d1c261e5    1.75 KiB         0.61 KiB
  e8fbf1e2fbf16e9d7.js
  dist/css/chunk-vendors.6b1b5fc2.css       643.07 KiB       89.40 KiB
  dist/css/app.6564fce6.css                 0.16 KiB         0.15 KiB
```

以下のように、高速化後はCode splittingされて各ファイルのサイズは小さくなっていました。
Code splittingの仕組みや生成コードに関しては「[ちゃんと理解するCode Splitting - Qiita](https://qiita.com/seya/items/06b160adb7801ae9e66f)」が詳しかったです。

```:高速化後
  File                                      Size             Gzipped

  dist/js/chunk-vendors.4899a913.js         257.19 KiB       84.77 KiB
  dist/js/chunk-57b10861.d78f63ba.js        223.01 KiB       72.55 KiB
  dist/js/chunk-1388286a.4144e147.js        117.97 KiB       35.20 KiB
  dist/js/chunk-75fdcd66.a640feae.js        106.27 KiB       31.20 KiB
  dist/js/chunk-ae9d74f2.9a4b9266.js        79.56 KiB        26.68 KiB
  dist/js/chunk-de20c21e.9462a166.js        42.10 KiB        13.58 KiB
  dist/js/chunk-c6eec2f4.490c54c6.js        40.36 KiB        11.39 KiB
  dist/js/chunk-252228c4.ef579924.js        25.09 KiB        4.11 KiB
  dist/js/chunk-6e83591c.92a60880.js        22.17 KiB        6.91 KiB
  dist/js/chunk-40ef8714.aef70fd8.js        19.92 KiB        5.56 KiB
  dist/js/chunk-079922cd.8eeb0fea.js        16.79 KiB        5.25 KiB
  dist/js/chunk-4465fa4c.889f9ca9.js        15.26 KiB        6.11 KiB
  dist/js/app.4a5ce537.js                   14.16 KiB        5.54 KiB
  dist/js/chunk-67a04d64.0157cdaf.js        11.99 KiB        3.71 KiB
  dist/js/chunk-3acce350.a0a0e1d5.js        4.61 KiB         1.67 KiB
  dist/precache-manifest.3f6d46fe310a44b    2.82 KiB         0.85 KiB
  a711688f1aac2c93d.js
  dist/js/chunk-2d0b1f6a.52cd552e.js        2.59 KiB         1.27 KiB
  dist/service-worker.js                    1.98 KiB         1.01 KiB
  dist/js/chunk-2d0ccfb8.815a5c6d.js        1.06 KiB         0.57 KiB
  dist/js/chunk-2d2100ac.72ca6675.js        0.95 KiB         0.52 KiB
  dist/js/chunk-2d0aef64.323dc465.js        0.33 KiB         0.25 KiB
  dist/css/chunk-vendors.37551f53.css       282.11 KiB       31.59 KiB
  dist/css/chunk-c6eec2f4.52a2d441.css      39.09 KiB        5.24 KiB
  dist/css/chunk-de20c21e.48c6bfcc.css      28.64 KiB        5.04 KiB
  dist/css/chunk-40ef8714.47b3a938.css      24.87 KiB        3.80 KiB
  dist/css/chunk-ae9d74f2.ad5a8e30.css      6.29 KiB         1.40 KiB
  dist/css/chunk-079922cd.764a2af9.css      6.14 KiB         1.34 KiB
  dist/css/app.e72ed97e.css                 0.02 KiB         0.04 KiB
```


非同期でimportするのも効果があったと思いますが、今回の場合は必要なアイコンのインポートに変更したときが体感でパフォーマンスが上がった気がしました。