JavaScript
aws-cli
vue.js
webpack
dayjs

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

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

で、少し気になって試しにGoogleのPageSpeed Insightsで計測してみたところ、なんとモバイルで26点だった。PCの方でも52点。

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


Google Fontsの読み込みの最適化

まずはこれ。Google Fontsはカッコイイけど、公式にあるCSSでの読み込みをするとめちゃくちゃ表示を遅くしてしまう。

AMP にも対応!Google Fonts を 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を使ってやっているので、デプロイ用のシェルスクリプトで設定することにした。


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はあまりキャッシュして欲しくなかったので、2行目でindex.htmlをコピーし直してCache-Controlをno-storeにしている。


バンドルされるjsファイルの分析

ここまでやって、次はjsファイルのパフォーマンス改善に入っていくのだけど、その前にどこをどう完全すれば良いのかを知っておかないといけない。ってことで、Webpack Bundle Analyzerを入れて、可視化をしていく。

$ npm install --save-dev webpack-bundle-analyzer


webpack.config.js

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

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



$ npx webpack

こんな感じで設定して起動するとhttp://127.0.0.1:8888/で確認できる。

スクリーンショットを取ってないのだけど、最初に確認したときは丸々一つのbundle.jsに全部乗せされていたので、これを小分けにして最適化していくことになる。最終的にはこうなった。

スクリーンショット 2019-02-25 15.50.01.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


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'
+ },
...
]


.babelrc

"plugins": [

"@babel/plugin-transform-runtime",
+ "@babel/plugin-syntax-dynamic-import"
],


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.jschunkFilenameを追加して、/* webpackChunkName: "dialog" */というコメントを埋め込むと、dialog.bundle.jsが新たに生成されるようになる。また、import()文を扱うために、@babel/plugin-syntax-dynamic-importというプラグインをインストールしている。

import()はPromiseを返して、引数のオブジェクトのdefaultプロパティに読み込んだ実体が入っているので、これを返すようにする。

で、Vueの機能で非同期でコンポーネントを読み込めるので、Dynamic Importと組み合わせることで、ダイアログやモーダル、表示されないページといった不要なコードを省きつつ、必要な時に読み込むことができるようになる。

動的 & 非同期コンポーネント — Vue.js

ちなみに、以前書いたダイアログ等の動的なコンポーネントの読み込みでは、以下のように書くことができる


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で設定すれば良い。


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へ乗り換えた

$ npm install dayjs --save


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

スクリーンショット 2019-02-25 16.16.54.png

まだデータ少ないけど、こんな感じで可視化されるのでありがたい。日々の計測大事。