Airレジ中国SPAを5分の1に軽くした話
李 立
Airレジ中国SPAを5分の1に軽くした話
対象読者
- SPA(Single Page Application)の性能を改善したい方
- webpack buildの流れを改善したい方
背景
私はAirレジ中国チームでフロントエンジニアとして開発を担当している李です。今回はSPAの性能改善について共有したいと思います。
本稿では、性能があまり良くない端末と弱電波の環境下でもSPA(Single Page Application)が速く走るように改善できたことをお伝えします。
私を含めエンジニアたちは性能の良い端末(MAC Proなど)でSPAの開発をしていたため、アプリケーションのパフォーマンスが悪いことに気付いていませんでした。
そしていざリリースしたところ、性能が良くない端末を使っているお客様から、「めっちゃ重い(太卡了)」と多くの苦言をいただいてしまいました。
「重い」というのはお客様からの直感的な体験ですが、この現状の裏には何があったのでしょうか?
分析してみましょう!
ストーリー
まず、webpack-bundle-analysis を使って分析してみよう
webpack-bundle-analysis とは、 webpack の build について分析するツールです。
webpack-bundle-analysis を使って以下の分析結果が出てきます。
この図を見ると、改善すべきのところがはっきりわかるでしょう。
特に大きいのはこれらのファイルです。
- Index.styl
- airui.min.css
- moment.js
- moment.time-zone.js
- lodash.js
- r-air-ui/build.js
あとは、いろいろな使われていない package などもありました。
全体的にsizeは 1.1MB ぐらいありました。
では、以上の結果に対して一つずつ改善しましょう。
CSS抽出 「1.1MB → 601KB」
まずは簡単なやつからメスを入れましょう、 Index.styl
と airui.min.css
この二つは stylus/css なので、js から抽出して独立させましょう。
webpack の config に以下のように設置します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.styl(us)?$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'stylus-loader'
]
},
]
}
こうすると、css/stylus は JavaScript から抽出されていくようになります。
css/stylus を抽出したら、 601KB に減りました!
Moment.js から date-fns.js に遷移 「601KB → 424KB」
Moment.js
は時間とか日付とかについての package です。使い勝手には別に問題がないのですが、size が非常に大きいので、開発者のみなさんからは文句が多く寄せられているようです。
Git Issue
このissueに対しては、 date-fns.js
をおすすめます、Moment.js
できることが全部できるし、size も小さい。では、Moment.js
から、date-fns.js
に移行しましょう!
Moment.js
から date-fns.js
に移行したら、API が違うので工数が結構かかりそうですが、でも、効果が絶大です。
そのおかげで、 size を効果的に削減できました。
jsonwebtoken.js から jwt-decode.js に移行 「424KB → 283KB」
ここでちょっと落ち着いて、もう一度 bundle を確認しましょう!
以上の図から見ると、いろんな暗号化、計算に関する package がたくさんあります。 例えば:
- elliptic.js
- bn.js
- asn1.js
- des.js
- hash.js
最初に、以上の package を見つけたとき、困惑しました。我々の SPA は複雑的な計算とか暗号化とかやっていないのに、これらの package は一体どこからimportされてきたのでしょうか?
間接的に依頼しているようだったので、計算に関する package を洗い出して、ようやく犯人を見つけました。
jsonwebtoken.js
です!
jsonwebtoken.js
は jwt
を実現しているための package なので、frontend 側だけじゃなく、backend 側の機能も実現されています。
だから、backend のためにいろんな計算が実装されているのです。
しかし、うちのSPAのため、ただ jwt
の decode 機能だけを使っています。他の機能は全く使っていないため、無駄になっています。
そして、jsonwebtoken.js
の代わりに、中の decode 機能だけを抽出している jwt-decode.js
という package を切り替えます!
これに切り替えたら、たくさん必要のなかった package がなくなりました!
lodash.js を分散 「283KB → 277KB」
lodash.js
も大きいですよね。
lodash.js
とは javascript のいろんな不足に補っている package ですが、我々の SPA ではすべでのlodash.js
機能を使っている訳ではないので、使っている機能だけimport すれば良いはずです。
1
2
3
4
5
6
7
// 元の書き方
import { isEqual } from 'lodash'
// isEqualだけの使い方
import isEqual from 'lodash.isequal'
そうすると、bundle の size は少しだけ下がりました。
細かいpackageを修正 277KB → 249KB
最後は、全ての package を点検して、使っていないものや重複しているものとを全部をつまみ出します!
我々の SPA の場合は babel-polyfill
と transform-runtime
両方もimportしていますが、重複していますので、
transform-runtime
をつまみ出します!(babel-polyfill
と transform-runtime
の関係はちょっと複雑なので、ここに参考になります)
es6-promise
はcore.js
に含まれており、重複していますので、
es6-promise
をつまみ出します
改善前と改善後の対比 1.1MB → 249KB
これらの施策を実施したところで、bundle の size を前後対比してみます。「1.1MB」から「249KB」まで削減できました!
さらにもう一歩
- code-splitting
今まで頑張りました結果はただ bundle の size を下げたことでした。
しかし当時の実装ではこの大きな bundle を全てダウンロードしきらないと最初の画面をみることはできませんでした。これは「重い」の原因の一つです。
この課題を解決して最初の画面をできるだけ速く見せるのためには、最初の画面の必要ファイルをダウンロードさせるようにするしかありません。
これは、code splitting という技術を使って実現できます。
1
2
3
4
5
6
7
8
9
// 元の書き方
import Menus from '../newPages/goodsMenu/Menus'
import MenusDetail from '../newPages/goodsMenu/MenusDetail'
// code splittingの書き方
const Menus = () => import('../newPages/goodsMenu/Menus')
const MenusDetail = () => import('../newPages/goodsMenu/MenusDetail')
webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
こうすることで、最初の画面で必要な source だけダウンロードされるようになりました。
なんと、最初の画面だけなら、195KB にまで削除できました!
JavaScript解釈時間対比 2.11s –> 0.9s
bundle の size を削減することで、ダウンロード時間だけでなく JavaScript の解釈時間も削減することができました。
改善前に JavaScript の解釈時間は 2.11s でしたが、改善したら以下の結果になりました。
JavaScript の解釈時間は 988ms と、1秒未満になり、50%以上削減できました!
結論
本稿では、Vue.jsでは一般的に検討される改善方法を我々のプロジェクトに適用したところを説明しました。
元の大きな bundle の size を1/5に削減しながら、最初の画面の JavaScript の解釈時間も半分以上削減しました。
確かに、webpack の config はとても複雑なものです。本稿で紹介したところだけではなく、プロジェクトによって改善すべきところが違います。具体的には webpack の公式サイトを参考にしてください。 特に optimization と performance というところです。性能に関する注意点がいろいろ紹介してあります。