30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

趣味で作っている SPA の Performance スコアを 40 点上げた

Lighthouse の Performance スコアを52から94に上げた。

Before。

f:id:numb_86:20201202072709p:plain

After。

f:id:numb_86:20201202072722p:plain

施策として具体的に何を行ったのか、書いていく。

経緯

以前、Shape Painter という SPA を作った。
構成はシンプルで、エントリポイントはひとつだけ。そしてそこで、ライブラリのコードをバンドルしたvendors.contenthash.jsと、アプリケーションのコードをバンドルしたindex.contenthash.jsを読み込んでいる。サーバとの通信はページ読み込み時のみで、あとはフロントエンドで完結して動作する。

React や Redux の習作という意味合いが強く、公開後は放っておいたのだが、なんとなく Tree ページを Lighthouse でスコアを計測してみたところ、Performance 項目がまさかの52だった。
パフォーマンスを意識せずに作っていたのは確かだが(useCallbackを使った最適化などは行っていた)、広告も入れてないしソーシャルウィジェットも入れていないのだから、もう少しマシだろうと思っていた。画像もほとんど使っていないし。

自分以外の利用者がほぼいないのが現状なので放っておいてもよかったのだが、ここ最近学んでいたパフォーマンス改善の実践として丁度よさそうなので、スコア改善に取り組むことにした。

テキストリソースの圧縮

Lighthouse は診断結果に応じたレポートを作成してくれるので、その内容を見ていく。

まず目を引いたのは、Enable text compression
テキストリソースを圧縮せずに配信しており、これを改善するだけでもファイルサイズを大幅に削減できそうである。

f:id:numb_86:20201202092630p:plain

Shape Painter のリソースは S3 に置いてあり、CloudFront で配信している。
この場合、CloudFront でリソースの圧縮を設定できる。

Edit Behavior で Compress Objects AutomaticallyYesにすると、コンテンツを自動的に圧縮して配信してくれる。

f:id:numb_86:20201202092640p:plain

Create Invalidationでキャッシュをクリアしたあとに確認したところ、無事にcontent-encodingvaryが設定されていた。

f:id:numb_86:20201202092650p:plain

vendors.contenthash.jsに対する効果が特に大きく、551kBから152kBにまでサイズを削減できた。

再度計測したところ、大幅にスコアが改善し、Enable text compressionの警告も消えた。

f:id:numb_86:20201202092710p:plain

テキストリソースの圧縮についての詳細は、以前書いた。

numb86-tech.hatenablog.com

script 要素に defer 属性を設定する

Lighthouse のレポートに書かれていたわけではないのだが、script要素にdefer属性をつけていないことに気付き、修正した。
具体的には、html-webpack-pluginの設定を変えた。

github.com

defer属性にどのような効果があるのかは、以下を参照。

numb86-tech.hatenablog.com

スコアは横ばいだったが、これ自体がやるべきことだったので、よしとする。
First Contentful PaintSpeed Indexも目に見えて改善した。

f:id:numb_86:20201202092722p:plain

ブラウザにキャッシュさせるようにする

Lighthouse のレポートにServe static assets with an efficient cache policyという警告が出ているので、次はそれに取り組む。

f:id:numb_86:20201202093737p:plain

Cache-Controlを使って効率的にキャッシュしましょうとのこと。
確かに現状ではCache-Controlを全く設定していない。

既に述べたように、ライブラリのバンドルファイルと、アプリケーションのバンドルファイルを読み込んでいる。
そしてどちらも、ファイル名にハッシュ値を使うことで、ファイルの中身が変わればファイル名も変わるようにしている。
これはキャッシュバスティングという手法で、リソースの内容が変わればリソースの URL も変わるため、古い内容のリソースを参照し続けることを回避できる。
そのため、キャッシュを長く設定しても問題ないように思える。ただ、先程の圧縮のケースのように、同じ URL でも配信内容が変わることもあるので、極端に長い時間は設定しないほうがいいかもしれない。
取り敢えず今回は、90 日間キャッシュするようにした。

また、いくらキャッシュバスティングを行っていても、HTML ファイルが古いままでは意味がない。
そのため、HTML ファイルだけは、キャッシュしないようにした。

Shape Painter は、masterブランチにコミットされた際に GitHub Actions でデプロイを行っている。
そのため、.github/workflows/deploy.ymlを編集して、S3 にリソースを設置する際にCache-Controlフィールドが付与されるようにした。

github.com

レスポンスヘッダにCache-Controlが付与され、2 回目以降のアクセスではキャッシュを使うようになっている(from memory cacheとなっている)。

f:id:numb_86:20201202092735p:plain

f:id:numb_86:20201202092745p:plain

スコアが上がり、Serve static assets with an efficient cache policyの警告も消えた。

f:id:numb_86:20201202092802p:plain

Cache-Controlそのものの説明は、以下を参照。

numb86-tech.hatenablog.com

webpack のコード分割

最後に、残っている警告であるRemove unused JavaScriptに取り組む。

未使用の JavaScript があるとのことだが、心当たりがある。
既に少し触れたが、ライブラリのコードは全てvendors.contenthash.jsにバンドルしている。だが意図があってそうしているわけではなく、webpack のドキュメントにあるサンプルの内容をただコピペしただけである。

    cacheGroups: {
      commons: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all',
      },
    },

こうして作られたバンドルファイルをページロード時に読み込んでいるのだが、明らかに、不要なコードも含まれているはず。
これを改善できれば、ページの読み込みや表示を高速化できると思われる。

まず、いい機会なので webpack をv5に上げた。

github.com

次に、webpack-bundle-analyzerで現状を確認する。

f:id:numb_86:20201202092816p:plain

html2canvasが目を引く。
これは、その名の通り任意の HTML 要素を Canvas 要素に変換してくれるライブラリ。
Shape Painter は、描画した図形をこのライブラリで Canvas 要素に変換し、そこからさらにtoBlobメソッドで PNG ファイルに変換することで、図形を画像ファイルとしてダウンロードできるようにしている。
つまり、このライブラリは画像のダウンロード時に必要になるもので、ウェブアプリの初期表示時には不要。このライブラリを Dynamic Import で読み込むようにすれば、ページロード時に読み込まれるファイルのサイズを削減できるはず。

それ以外にも、React Router のルーティング単位でのコード分割を導入したり、webpack.config.jsの設定を見直したりした。

github.com

その後も、初期表示時に不要なファイル(モーダルなど)を Dynamic Import で読み込むようにして、コード分割を進めた。
最終的に、以下のような形になった。

f:id:numb_86:20201202092833p:plain

その結果、冒頭で述べたようにスコアは94まで改善された。

webpack によるコード分割については、以下の記事に詳しく書いた。

numb86-tech.hatenablog.com

感想

パフォーマンス改善というとテクニカルな手法を駆使するイメージがあったが、当たり前のことを当たり前にやるだけでもスコアが改善されることが分かった。

gzipによる圧縮やレスポンスヘッダの設定などはバックエンドっぽいというか、あまりフロントエンドエンジニアが行うイメージがなかったのだが、今回のような構成ではフロントエンドエンジニアが行うことが多いと思う。
「これはフロントエンド領域、これはバックエンド領域」のように決め付けず、幅広く学んでおかないと、いざという時に対応できないように感じた。というより、CDN による配信やキャッシュの設定も、「フロントエンド」に含まれていると認識すべきなのかもしれない。CDN Edge の利用も、History APIのフォールバックのために必要だったりするし。

webpack のコード分割の初歩

JavaScript や TypeScript を使ってウェブアプリを提供する場合、開発時はimportexportなどの ES Modules を使い、公開時はファイルをバンドルして公開することが多い。
以下の記事に書いたように、現在の主要なブラウザは ES Modules に対応してものの、バンドルせずに公開してしまうとパフォーマンスに悪影響を与える可能性がある。

numb86-tech.hatenablog.com

ファイル数が増えれば増えるほど影響は深刻になるため、依存関係が深いライブラリを使っている場合などは、レイテンシが飛躍的に増加してしまう。
そのため、バンドルせずに公開するのは現実的ではない。

バンドルしてひとつのファイルにまとめてしまえば、JavaScript のダウンロードは一度で済む。
しかしそうすると今度は、バンドルファイルの肥大化という問題が発生する。
巨大なファイルはダウンロードやパースに時間がかかるため、ユーザ体験を悪化させてしまう。

この問題を解決するために、主要なモジュールバンドラにはコードを分割するための機能が備わっている。
この機能を適切に使用することで、バンドルファイルのサイズを削減し、パフォーマンスを改善することができる。

この記事では、webpack でコード分割を行うにはどうすればよいのか、具体的にどのような形に分割されるのか、などを見ていく。

使用したライブラリのバージョンは以下の通り。

  • webpack@5.6.0
  • webpack-cli@4.2.0
  • clean-webpack-plugin@3.0.0
  • html-webpack-plugin@5.0.0-alpha.14
  • nodemon@2.0.6
  • react@17.0.1
  • react-dom@17.0.1
  • @babel/core@7.12.7
  • @babel/preset-react@7.12.7
  • babel-loader@8.2.1
  • webpack-bundle-analyzer@4.1.0
  • lodash@4.17.20

環境構築

まず、検証用の環境を準備する。

以下のpackage.jsonを用意してから$ yarnを実行して、必要なライブラリをインストールする。

{
  "name": "chunk",
  "license": "MIT",
  "scripts": {
    "start": "nodemon server.js",
    "build": "NODE_ENV=production webpack"
  },
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^5.0.0-alpha.14",
    "nodemon": "^2.0.6",
    "webpack": "^5.6.0",
    "webpack-cli": "^4.2.0"
  }
}

webpack.config.jsは以下のようにする。
この時点では、コード分割に関する設定は入れていない。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

const config = {
  entry: {
    index: './src/index.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
  },
  plugins: [
    new HtmlWebpackPlugin({
      scriptLoading: 'defer',
    }),
    new CleanWebpackPlugin(),
  ],
}

module.exports = config;

最後に、server.js
HTML ファイルは即時レスポンスを返すが、JavaScript ファイルについては3秒後にレスポンスを返す。
こうすることで、挙動を確認しやすくしている。

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
  switch(true) {
    case /^\/$/.test(req.url):
      fs.readFile('./dist/index.html', 'utf-8', (err, data) => {
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.write(data);
        res.end();
      })
      break;
    case /\.js$/.test(req.url):
      fs.readFile(`./dist${req.url}`, 'utf-8', (err, data) => {
        setTimeout(() => {
          res.writeHead(200, {'Content-Type': 'text/javascript'});
          res.write(data);
          res.end();
        }, 3000)
      })
      break;
    default:
      res.writeHead(404);
      res.end();
  };
}).listen(8080);

まずはこの状態で検証を行う。必要に応じて設定を追加、変更していく。

Dynamic Import によるコード分割

Dynamic Import を使うと、webpack が自動的にコードを分割してくれる。

まずは Dynamic Import を使わずにコードを書いてみる。

// src/a.js
const elem = document.createElement('div');
elem.textContent = 'execute a';
document.body.appendChild(elem);

export const a = 1;
// src/index.js
import {a} from './a.js';

const elem = document.createElement('button');
elem.textContent = 'click';
elem.addEventListener('click', () => {
  alert(a);
})

document.body.appendChild(elem);

この状態でビルド($ yarn build)すると、dist/index.bundle.jsが出力される。
このファイルは、src/index.jssrc/a.jsをバンドルしたものなので、読み込まれると両方のファイルの内容が実行される。

$ yarn startでサーバを起動して、確認してみる。
http://localhost:8080/にアクセスすると、以下のようになる。

f:id:numb_86:20201201050601g:plain

index.bundle.jsが実行されるとsrc/a.jsの内容も実行されるため、execute aが挿入される。

次に、Dynamic Import を使った形に書き換えてみる。
Dynamic Import の基本的な使い方については、以下に書いた。

numb86-tech.hatenablog.com

src/index.jssrc/a.jsimportしているが、そこで使われているaという値は、ボタンが押下されたときに必要になる。
そのため、初期読み込み時にはimportが不要であり、Dynamic Import を使うことができる。
以下のコードでは、ボタンが押下された時に動的にsrc/a.jsを読み込むようにしている。

// src/index.js
const elem = document.createElement('button');
elem.textContent = 'click';
elem.addEventListener('click', () => {
  import('./a.js').then(res => {
    alert(res.a);
  })
})

document.body.appendChild(elem);

この状態でビルドすると、index.bundle.jsの他に、85.bundle.jsが生成されている。
これが、コード分割である。ふたつのバンドルファイルに分割されている。

挙動は以下のようになる。

f:id:numb_86:20201201050650g:plain

HTML ファイルのhead要素に<script defer="defer" src="/index.bundle.js"></script>があるのでindex.bundle.jsが読み込まれるが、このファイルにはsrc/a.jsの内容は含まれていないため、execute aは挿入されない。
ボタンを押下したタイミングで85.bundle.jsのダウンロードが始まり、その後ファイルが実行され、execute aが挿入される。
より具体的には、ボタンを押下するとhead要素にscript要素が追加される。そして読み込みが終わると、追加されたscript要素は削除される。

f:id:numb_86:20201201050742g:plain

ダウンロードは一度のみ行われるので、ボタンを複数回押下しても重複してダウンロードされることはない。

今回は JavaScript のダウンロードに3秒かかるようにしているので却って使い勝手が悪くなったが、コード分割を上手く使うことで、「ページロード時は必要最低限の JavaScript コードだけを読み込み、それ以外のコードは必要になったタイミングで読み込む」ということが可能になる。

prefetch

先程の例では、ボタンを押下したタイミングで85.bundle.jsのダウンロードが開始されるため、ボタンを押下してからアラートが表示されるまで、必ず3秒以上かかってしまう。
prefetchを使うことで、この問題を解決できる。

/* webpackPrefetch: true*/というコメントを追加することで、prefetchが有効になる。

 const elem = document.createElement('button');
 elem.textContent = 'click';
 elem.addEventListener('click', () => {
-  import('./a.js').then(res => {
+  import(/* webpackPrefetch: true*/ './a.js').then(res => {
     alert(res.a);
   })
 })

index.bundle.jsのダウンロードと実行が終わったタイミングで<link rel="prefetch" as="script" href="/85.bundle.js">が追加され、85.bundle.jsのダウンロードが開始される。
ページの初期化に必要なindex.bundle.jsの実行をまず行い、それが終わったら85.bundle.jsをダウンロードしておく。
こうすることで、ページの読み込みを最適化しつつ、ボタンを押下した時の待ち時間を短縮できる。

事前に行われるのはダウンロードのみで実行はされない。ボタンを押下したタイミングで、ファイルが実行されてexecute aが挿入される。

f:id:numb_86:20201201050840g:plain

分割されたファイルに名前をつける

デフォルトだと、先程の85.bundle.jsのように数字がファイル名になる。
このままだと分かりづらいので、設定を変えていく。

まず、webpack.config.jsoptimization.chunkIds'named'にすると、パスに基づいて自動的に名前がつけられる。

     }),
     new CleanWebpackPlugin(),
   ],
+  optimization: {
+    chunkIds: 'named'
+  }
 }

今回の例の場合、output.filenameの設定と合わさって、src_a_js.bundle.jsという名前になる。

通常のバンドルファイルとは別のルールを適用させたい場合は、output.chunkFilenameを使う。
以下のようにすると、src_a_js.foo.jsという名前になる。

   },
   output: {
     filename: '[name].bundle.js',
+    chunkFilename: '[name].foo.js',
     path: path.resolve(__dirname, 'dist'),
     publicPath: '/',
   },

chunkFilenameには関数を渡すことも可能。
以下の場合はsrc~a~js.async.jsという名前になる。

   },
   output: {
     filename: '[name].bundle.js',
-    chunkFilename: '[name].foo.js',
+    chunkFilename: (pathData) => {
+      return pathData.chunk.id.replace(/_/g, '~') + '.async.js';
+    },
     path: path.resolve(__dirname, 'dist'),
     publicPath: '/',
   },

Dynamic Import にwebpackChunkNameというコメントを書くことで、個別に名前をつけることもできる。
この場合、optimization.chunkIdsの設定は無視される。
以下のようにすると、先程のchunkFilenameの設定と合わさってx~y-z.async.jsという名前になる。

import(/* webpackChunkName: 'x_y-z', webpackPrefetch: true*/ './a.js')

ライブラリのコードを別のファイルに分割する

エントリポイントが複数ありそれぞれで同じライブラリを使っていた場合、重複してバンドルされ、バンドルファイルが肥大化してしまう。
コード分割は、これを回避するためにも使われる。

以降は、React アプリを作って動作確認していく。

まず、package.jsonを以下の内容にする。
必要なライブラリをインストールする他、buildコマンドに--analyzeオプションをつけてバンドル結果を見れるようにした。

{
  "name": "chunk",
  "license": "MIT",
  "scripts": {
    "start": "nodemon server.js",
    "build": "NODE_ENV=production webpack --analyze"
  },
  "devDependencies": {
    "@babel/core": "^7.12.7",
    "@babel/preset-react": "^7.12.7",
    "babel-loader": "^8.2.1",
    "clean-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^5.0.0-alpha.14",
    "nodemon": "^2.0.6",
    "webpack": "^5.6.0",
    "webpack-bundle-analyzer": "^4.1.0",
    "webpack-cli": "^4.2.0"
  },
  "dependencies": {
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  }
}

次に、webpack.config.js
JavaScript ファイルを Babel で処理するようにした他、エントリポイントにanotherを追加した。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

const config = {
  entry: {
    index: './src/index.js',
    another: './src/another.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
  },
  module: {
    rules: [{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}],
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      chunks: ['index'],
      scriptLoading: 'defer',
    }),
    new HtmlWebpackPlugin({
      filename: 'another.html',
      chunks: ['another'],
      scriptLoading: 'defer',
    }),
    new CleanWebpackPlugin(),
  ],
  optimization: {
    chunkIds: 'named'
  }
}

module.exports = config;

babel.config.jsは以下のようにした。

module.exports = {
  presets: [
    [
      '@babel/preset-react',
      {
        development: process.env.NODE_ENV === 'development',
        'runtime': 'automatic'
      }
    ]
  ]
};

最後に、server.jsに以下の変更を加えて、/anotherのルーティングを追加する。

         res.end();
       })
       break;
+    case /^\/another$/.test(req.url):
+      fs.readFile('./dist/another.html', 'utf-8', (err, data) => {
+        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
+        res.write(data);
+        res.end();
+      })
+      break;
     case /\.js$/.test(req.url):
       fs.readFile(`./dist${req.url}`, 'utf-8', (err, data) => {
         setTimeout(() => {

環境は整ったので、早速コードを書いていく。

// src/index.js
import {render} from 'react-dom';

const App = () =>
  <div>
    index<br />
    <a href="/another">to another</a>
  </div>;

render(<App />, document.body);
// src/another.js
import {render} from 'react-dom';

const App = () =>
  <div>
    another<br />
    <a href="/">to index</a>
  </div>;

render(<App />, document.body);

この状態でビルドするとindex.bundle.jsanother.bundle.jsが出力されるが、両方にreactreact-domが含まれてしまい、無駄が生まれている。

f:id:numb_86:20201201050056p:plain

http://localhost:8080/からhttp://localhost:8080/anotherに遷移した場合、ライブラリのコードが二重に読み込まれることになる。

webpack.config.jsoptimization.splitChunks.chunk'all'に設定すると、ライブラリのコードを自動的に分割してくれるようになる。

     new CleanWebpackPlugin(),
   ],
   optimization: {
-    chunkIds: 'named'
+    chunkIds: 'named',
+    splitChunks: {
+      chunks: 'all',
+    },
   }
 }

これでビルドした結果が以下。

f:id:numb_86:20201201050048p:plain

vendors-node_modules_react-dom_index_js-node_modules_react_jsx-runtime_js.bundle.jsが生成され、ライブラリのコードはそこに含まれている。
index.bundle.jsanother.bundle.jsからはライブラリのコードが取り除かれているため、重複が解消されている。
ライブラリのバンドルコードをキャッシュさせるなどすれば、パフォーマンスの向上が見込める。

なお、この設定を行うと、エントリポイントがひとつしかない場合でも、ライブラリのコードを分割してくれる。

HTML Webpack Plugin はコード分割に対応しているため、自動的に以下のscript要素をindex.htmlに挿入してくれる。
分割したファイルを手動で読み込ませる必要はない。

<script defer="defer" src="/vendors-node_modules_react-dom_index_js-node_modules_react_jsx-runtime_js.bundle.js"></script>
<script defer="defer" src="/index.bundle.js"></script>

名前付け

optimization.splitChunks.nameで、ファイル名を指定できる。
以下ではfooと指定しているので、output.filenameの設定と組み合わさり、foo.bundle.jsという名前になる。

  optimization: {
    chunkIds: 'named',
    splitChunks: {
      chunks: 'all',
      name: 'foo',
    },
  }

以降の例では、optimization.splitChunks.nameは何も設定せずにビルドしている。

使用状況に応じたコードの分割

先程の例では、両方のエントリポイントが同じライブラリに依存していた。
では、片方のエントリポイントのみが依存しているライブラリがある場合は、どうなるのか。

lodashをインストールし、それをsrc/index.jsでのみ使うようにすることで、確認してみる。

import {render} from 'react-dom';
import _ from 'lodash';

const App = () =>
  <div>
    another<br />
    {`${_.join(['Hello', 'webpack'], '-')}`}<br />
    <a href="/">to index</a>
  </div>;

render(<App />, document.body);

この状態でビルドすると、vendors-node_modules_react-dom_index_js-node_modules_react_jsx-runtime_js.bundle.jsの他に、vendors-node_modules_lodash_lodash_js.bundle.jsが生成される。

f:id:numb_86:20201201050028p:plain

後者は、http://localhost:8080/にアクセスしたときにのみ、読み込まれる。

<!-- http://localhost:8080/ -->
<script defer="defer" src="/vendors-node_modules_react-dom_index_js-node_modules_react_jsx-runtime_js.bundle.js"></script>
<script defer="defer" src="/vendors-node_modules_lodash_lodash_js.bundle.js"></script>
<script defer="defer" src="/index.bundle.js"></script>
<!-- http://localhost:8080/another -->
<script defer="defer" src="/vendors-node_modules_react-dom_index_js-node_modules_react_jsx-runtime_js.bundle.js"></script>
<script defer="defer" src="/another.bundle.js"></script>

このように、webpack が最適な形で分割してくれる。

cacheGropes.***.test で、分割するライブラリを指定する

再びエントリポイントをsrc/index.jsのみにして、ビルドしてみる。
そうすると、index.bundle.jsと、全てのライブラリのコードを含むバンドルファイルが、生成される。
この挙動は既に説明した通りである。

f:id:numb_86:20201201050018p:plain

cacheGropes.***.testを使うと、ライブラリを一緒くたにまとめてしまうのではなく、指定したライブラリだけを分割することが可能になる。

例えば、react-domlodashを分割したい場合は、以下のように書く。
パスの区切り文字を[\\/]にしているのは、クロスプラットフォームに対応するため。

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        reactDom: {
          test: /[\\/]node_modules[\\/]react-dom[\\/]/,
          name: 'react_dom',
        },
        lodash: {
          test: /[\\/]node_modules[\\/]lodash[\\/]/,
          name: 'lodash',
        }
      }
    },
  }

この状態でビルドすると、index.bundle.jsの他に、react_dom.bundle.jslodash.bundle.jsが生成される。

f:id:numb_86:20201201045923p:plain

react-domlodashだけがそれぞれに分割され、それ以外のライブラリはindex.bundle.jsに含まれている。