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 を使って以下の分析結果が出てきます。

beforeImprove

この図を見ると、改善すべきのところがはっきりわかるでしょう。

特に大きいのはこれらのファイルです。

  • 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.stylairui.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 から抽出されていくようになります。

afterCSS

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 が違うので工数が結構かかりそうですが、でも、効果が絶大です。

afterMomentjs

そのおかげで、 size を効果的に削減できました。

jsonwebtoken.js から jwt-decode.js に移行 「424KB → 283KB」

ここでちょっと落ち着いて、もう一度 bundle を確認しましょう!

afterMomentjs

以上の図から見ると、いろんな暗号化、計算に関する package がたくさんあります。 例えば:

  • elliptic.js
  • bn.js
  • asn1.js
  • des.js
  • hash.js

最初に、以上の package を見つけたとき、困惑しました。我々の SPA は複雑的な計算とか暗号化とかやっていないのに、これらの package は一体どこからimportされてきたのでしょうか?

間接的に依頼しているようだったので、計算に関する package を洗い出して、ようやく犯人を見つけました。

jsonwebtoken.js です!

jsonwebtoken.jsjwt を実現しているための package なので、frontend 側だけじゃなく、backend 側の機能も実現されています。

だから、backend のためにいろんな計算が実装されているのです。

しかし、うちのSPAのため、ただ jwt の decode 機能だけを使っています。他の機能は全く使っていないため、無駄になっています。

そして、jsonwebtoken.js の代わりに、中の decode 機能だけを抽出している jwt-decode.js という package を切り替えます!

removeJWT

これに切り替えたら、たくさん必要のなかった 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 は少しだけ下がりました。

removeLodash

細かいpackageを修正 277KB → 249KB

最後は、全ての package を点検して、使っていないものや重複しているものとを全部をつまみ出します!

我々の SPA の場合は babel-polyfilltransform-runtime 両方もimportしていますが、重複していますので、 transform-runtime をつまみ出します!(babel-polyfilltransform-runtimeの関係はちょっと複雑なので、ここに参考になります)

es6-promisecore.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 だけダウンロードされるようになりました。

afterCodeSplitting

なんと、最初の画面だけなら、195KB にまで削除できました!

JavaScript解釈時間対比 2.11s –> 0.9s

bundle の size を削減することで、ダウンロード時間だけでなく JavaScript の解釈時間も削減することができました。

beforeSpeed

改善前に JavaScript の解釈時間は 2.11s でしたが、改善したら以下の結果になりました。

afterSpeed

JavaScript の解釈時間は 988ms と、1秒未満になり、50%以上削減できました!

結論

本稿では、Vue.jsでは一般的に検討される改善方法を我々のプロジェクトに適用したところを説明しました。

元の大きな bundle の size を1/5に削減しながら、最初の画面の JavaScript の解釈時間も半分以上削減しました。

確かに、webpack の config はとても複雑なものです。本稿で紹介したところだけではなく、プロジェクトによって改善すべきところが違います。具体的には webpack の公式サイトを参考にしてください。 特に optimization と performance というところです。性能に関する注意点がいろいろ紹介してあります。