Vueで作ったSPAの表示速度改善でやったこと

Vue.jsを全く触ったことないところから始めて、SPAのWebサービスを2ヶ月でリリースできたのだけど、同僚から「ちょっと表示が遅いですね」と言われた。それまではとにかく機能を動かすのとソースコードの見通しを悪くしないようにというのだけ気にしてて、パフォーマンスをあまり気にしていなかった。

で、少し気になって試しにGoogleの[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/)で計測してみたところ、なんとモバイルで26点だった。PCの方でも52点。

これはマズイと思い、いろいろ試行錯誤してモバイル72点、PCは98点まで改善したので、以下したことをまとめておく。

#Google Fontsの読み込みの最適化
まずはこれ。Google Fontsはカッコイイけど、公式にあるCSSでの読み込みをするとめちゃくちゃ表示を遅くしてしまう。
[AMP にも対応！Google Fonts を preload で先読みし最適化](https://firstlayout.net/optimize-google-fonts-with-preload/)

上記のサイトを参考に使っているフォントをダウンロードしてCDNにおき、CSSに`@font-face`を追加した。念のため`font-display: swap;`も指定しておく。

#Cache-Controlの設定
今回のサービスはFEのjsファイルをS3に置いてCloudFrontから配信する形をとっている。で、自分が思いっきり勘違いしていたのがCache-Control。BehaviorのObject Cachingの項目で設定した値をCache-Controlでも使ってくれるのかと思ってたら、そもそもCache-Controlのヘッダー自体吐いていない＼(^o^)／

実際にはObject CachingはCloudFrontがS3から取得したレスポンスをどのくらいの期間キャッシュしておくかの設定で、Cache-ControlはS3の元ファイルのメタデータで設定してあげないといけない。

jsファイルのデプロイはaws-cliを使ってやっているので、デプロイ用のシェルスクリプトで設定することにした。

```bash

aws s3 sync --delete dist/ s3://${bucket_name} --cache-control "max-age=86400"
aws s3api copy-object --bucket "${bucket_name}" --copy-source "${bucket_name}/index.html" --key "index.html" --metadata-directive "REPLACE" --content-type "text/html" --cache-control "no-store"
```

こんな感じでsyncコマンドのオプションでCache-Controlを設定できる。1行目だけだと全ファイルにCache-Controlが設定されるが、リリースが頻繁でindex.htmlはあまりキャッシュして欲しくなかったので、２行目でindex.htmlをコピーし直してCache-Controlを`no-store`にしている。

#バンドルされるjsファイルの分析
ここまでやって、次はjsファイルのパフォーマンス改善に入っていくのだけど、その前にどこをどう完全すれば良いのかを知っておかないといけない。ってことで、[Webpack Bundle Analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer)を入れて、可視化をしていく。

```bash
$ npm install --save-dev webpack-bundle-analyzer
```
```javascript:webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
```
```bash

$ npx webpack
```
こんな感じで設定して起動すると`http://127.0.0.1:8888/`で確認できる。
スクリーンショットを取ってないのだけど、最初に確認したときは丸々一つの`bundle.js`に全部乗せされていたので、これを小分けにして最適化していくことになる。最終的にはこうなった。
![スクリーンショット 2019-02-25 15.50.01.png](https://qiita-image-store.s3.amazonaws.com/0/34515/8bac4643-97a6-26a2-d8a8-90e1e1cdc28c.png)


#webpackのDynamic ImportとVueの非同期コンポーネント
FEのjsをまるごと1つのファイルにまとめていると、確かにリクエスト数は減るが毎回不要なコードも大量に含まれたファイルをダウンロードしなくてはいけなくなってしまう。

これを解決するのがwebpackのDynamic Importで、ファイルの先頭で`import from〜`とするのではなく、`import()`といった感じで関数でファイルをimportして使うようにする。

また、このimportの()内にコメントを埋めることで、生成されるjsファイルを分けることができる。

```
$ npm install bundle-loader --save
$ npm install --save-dev @babel/plugin-syntax-dynamic-import
```
```diff:webpack.config.js
module.exports = {
  context: __dirname + '/src',
  entry: './main.js',
  output: {
    filename: 'js/bundle.js',
+    chunkFilename: '[name].[hash].bundle.js',
    path: __dirname + '/dist',
    publicPath: '/'
  },
  ...
  module: {
    rules: [
        ...
+       {
+         test: /\.bundle\.js$/,
+         use: 'bundle-loader'
+       },
        ...
    ]
```
```diff:.babelrc
"plugins": [
  "@babel/plugin-transform-runtime",
+ "@babel/plugin-syntax-dynamic-import"
],
```
```diff:Home.vue
import Header from '@/components/modules/Header.vue';
import Footer from '@/components/modules/Footer.vue';
- import Dialog from '@/components/modules/Dialog.vue';

export default {
    name: 'Home',
    components: {
        Header,
        Footer,
+        Dialog: (resolve) => import(/* webpackChunkName: "dialog" */ '@/components/modules/Dialog.vue').then(component => resolve(component.default))
    },
    ...
}
```
`webpack.config.js`に`chunkFilename`を追加して、`/* webpackChunkName: "dialog" */`というコメントを埋め込むと、`dialog.bundle.js`が新たに生成されるようになる。また、import()文を扱うために、`@babel/plugin-syntax-dynamic-import`というプラグインをインストールしている。

import()はPromiseを返して、引数のオブジェクトのdefaultプロパティに読み込んだ実体が入っているので、これを返すようにする。
で、Vueの機能で非同期でコンポーネントを読み込めるので、Dynamic Importと組み合わせることで、ダイアログやモーダル、表示されないページといった不要なコードを省きつつ、必要な時に読み込むことができるようになる。
[動的 & 非同期コンポーネント — Vue.js](https://jp.vuejs.org/v2/guide/components-dynamic-async.html#%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88)

ちなみに、以前書いた[ダイアログ等の動的なコンポーネントの読み込み](https://qiita.com/renoinn/items/042ebd32383ad104657b)では、以下のように書くことができる

```javascript:DialogHelper.js
const DialogHelper = {
    showDialog (context, { message, ok, cancel }) {
        import(/* webpackPreload: true */ /* webpackChunkName: "dialog" */ '@/components/Dialog.vue')
            .then(component => {
                let DialogVM = Vue.extend(component.default);
                let dialog = new DialogVM({
                    parent: context,
                    propsData: {
                        message: message,
                        onPrimary () {
                            ok();
                            dialog.close();
                        },
                        onSecondary () {
                            cancel();
                            dialog.close();
                        }
                    }
                });
                dialog.show();
            });
    }
}
```
#splitChunksの追加
Dynamic Importで後から読み込めば良いファイルを分けたので、次はVue本体のようなvender系を分ける。これはwebpackで設定すれば良い。

```diff:webpack.config.js
module.exports = {
  context: __dirname + '/src',
  entry: './main.js',
  output: {
    filename: 'js/bundle.js',
    chunkFilename: '[name].bundle.js',
    path: __dirname + '/dist',
    publicPath: '/'
  },
+  optimization: {
+    splitChunks: {
+      name: 'vender',
+      chunks: 'initial'
+    }
+  },
```
これで、`vender.bundle.js`にVue本体やVue-Router、axios等がバンドルされるようになる。

#moment.jsをdayjsに変更
意外と重かったのがこのmoment.js。`ja`のみを使うように設定しても、それでもまだかなりの大きさだった。使っている部分はごく一部だけだったので、何か置き換えられないか探してたら、moment.js互換を謳うdayjsに行き着いた。
[日付時刻操作ライブラリをmomentからdayjsへ乗り換えた](https://qiita.com/yagi_suke/items/2848c8981ea6d9f26587)

```
$ npm install dayjs --save
```
```javascript

import dayjs from 'dayjs';
import 'dayjs/locale/ja';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.locale('ja');
dayjs.extend(relativeTime);

if (dayjs(value).isBefore(now, 'week')) {
    return dayjs(value).fromNow();
}
```
導入はこんな感じ。本体の他に`'dayjs/locale/ja'`を読み込んでロケーションを設定、moment.jsで`fromNow()`を使っていたので、relativeTimeプラグインを使っている。

今回行った対応はこんな感じ。これで完璧っていうわけではないけど、モバイル72点、PCは98点まで上がったのでひとまず良しとする。また、ページのパフォーマンス計測に`gas-webpagetest`を使わせていただいた。

[gas-webpagetestでWebPagetestのパフォーマンス計測を自動化、可視化する | Web Scratch](https://efcl.info/2018/10/22/gas-webpagetest/)

![スクリーンショット 2019-02-25 16.16.54.png](https://qiita-image-store.s3.amazonaws.com/0/34515/7ddff227-aa54-89b0-f071-2b053b3f1098.png)
まだデータ少ないけど、こんな感じで可視化されるのでありがたい。日々の計測大事。