【UX向上委員会】コード分割で初回ロードをはやくする


大きなプロジェクトになるほどバンドラーが生成するメインファイルが大きくなって初回ロードが遅くなってgoogle speed insightの点数が悪いことはないでしょうか？

そんな時はバンドラーでコード分割をしてファーストビューに必要なファイルだけを取得するようにしましょう。

## バンドラーとは
モジュールによって分割されたファイルを一つにまとめることができます。GulpやWebpackがそれにあたります。

利用するメリット主には
- HTTP/1.1では一度に処理できるリクエストの数に限界があるため、リクエストをまとめて回数を減らすことでパフォーマンス向上。
- ブラウザーはモジュールを使う際にrequireやimportを行う必要がない(ブラウザによってはESmoduleに対応していないため、webpackのようなmoduleバンドラーを使用する必要がある。)

ファイルのバンドル以外にもファイルの圧縮やCSSにブラウザことのprefixを付与したりできます。

この記事では主にwebpackについて解説します。

## Webpackとは

用途としてはバンドラーとして複数のファイルを一つにまとめることですが、コンパイルや画像の圧縮などできる事が様々。似たようなものにGulpがあります。

## 肥大化するバンドルファイル

通常、ReactなどでつくられたSPAをwebpackでバンドルすると全てのファイルが一つにまとめられ、ユーザは初回ロード時に全てのファイルをダウンロードすることになります。

しかし、アプリケーションが肥大化するにつれてバンドルファイルが大きくなり初回ロード時間が長くなってしまいます。

グーグルによると、ファーストビューがレンダリングされるまでに3秒以上かかると53％が閲覧を止めて離脱してしまうらしい。

## なぜコード分割

初回ロードのデータを削減することで、ロード時間を短縮できUXを向上させる事ができます。

例えば、SPAでユーザーがログインページに訪れたとします。

そうするとmain.jsがロードされます。しかし、main.jsにはloginページの他にaboutページやcontactページのファイルデータが含まれます。これらのページはユーザーが訪れない可能性があります。

main.jsが100KBあるとして、loginページは2KBだとすると、残り98KBはユーザーが今必要ないデータになります。

そうすると、main.jsを”初回ロード時に必要なファイル”と”ユーザーの要求に応じたファイル”に分けたほうがUXを損なわずにファイルをロードする事ができます。

ここからはコード分割を可能にする機能を紹介します。

まず準備から。

## 準備

### webpackの出力ファイルサイズを視覚化してくれるパッケージをインストール

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

```javaScript:webpack.config.js

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

module.exports = {
  ...,//
  plugins: [

    new BundleAnalyzerPlugin()
  ],
  ...,
}

```

### ファイル名をパスやモジュール名で出力

デフォルトだと分割したファイルは[数字].jsで出力されます。

なんのファイルかわかり辛いので、ファイル名をモジュールの名前やパスで出力してくれるようにします。

```javaScript:webpack.config.js

module.exports = {
  ...,//
 　optimization: {
    chunkIds: 'named'
  },
  ...,
}

```

## Dynamic import

動的にファイルを読み込む事ができます。

次の例をみてみましょう。

```JavaScript:index.js

// import { showTheTime } from "./showTheTime";
const button = document.createElement("button")
button.textContent = 'click';

button.addEventListener("click", async () => {
  const { showTheTime } = await import("./showTheTime");
  showTheTime()
})

document.body.appendChild(button);
```

ボタンをクリックすると`showTheTime`というファイルがimportされて時間が表示されるという関数があります（今回はファイルサイズの違いが分かり易いように`showTheTime`はmoment.jsを使用しています）。

![dynamic-importした時はクリック後ファイルがダウンロードされる](//images.ctfassets.net/nwdyo7rxqvgg/7jcYJcsqoRxSO44cMrkBmD/1d8becd1b399863f6dc504bf97c5d351/dynamic-import.gif)

ボタンをクリックすると`vandors-node_modules_mo...`と`src_showTheTime_js-node...`というファイルが取得されたのがわかります。

これはバンドル時dynamic import構文に出くわすとファイルを分割し、「クリックしたら分割したファイルを使用する」というロジックをwebpackが自動で生成しているからです。

↓通常のimportだとファイルはmain.jsに含まれているので、クリックしてもデータが取得されることはないです。

![dynamic-importしてない時は初回ロード時に全てのファイルがフェッチされる](//images.ctfassets.net/nwdyo7rxqvgg/2u3EpkDydAqhHkirNfnIzM/c49b17def4896f69a4e1cbe7dfbac41c/no-dynamic-import.gif)

```markdown:ファイルサイズ比較
//通常のimportの場合
main.js
-----
Stat size:689.22KB
Parsed size:296.79KB
Gzipped size: 74.46KB

//dynamic importの場合
main.js
-----
Stat size:25.78KB
Parsed size:10.35KB
Gzipped size: 4.03KB
```

moment.jsという重めのファイルを使用しているのでmain.jsを__70.43KB__(Gzipped size)減らす事ができました。

## Prefetch（webpack v4.6以降）

Prefetchフラグを使うとmain.jsの取得が終わり次第、分割したコードの取得を始めます。

将来のナビゲーションにおそらく必要となるファイルを取得する際に使用されます。

方法は簡単で、importの引数にコメントアウトした `webpackPrefetch: true`をパスの前に入れるだけです。

```JavaScript:index.js

// import { showTheTime } from "./showTheTime";
const button = document.createElement("button")
button.textContent = 'click';

button.addEventListener("click", async () => {
  const { showTheTime } = await import(/* webpackPrefetch: true */ "./showTheTime");
  showTheTime()
})

document.body.appendChild(button);

```

prefetchフラグがたてられたファイルは、headタグへリンクとして`<link　ref="prefetch">`のように挿入されます。

![prefetchパラメータを使うとヘッドにprefetchするリンクタグが挿入されている](//images.ctfassets.net/nwdyo7rxqvgg/lyjuq0bPmexN5W3j2KudO/61f0ea2e4515c59a6c50813b31ad2ff8/prefetch-link-tag.png)
 *headタグにlinkタグが新たに挿入されている*

 これらの `rel=prefetch`が付いたlinkタグはブラウザがアイドリング状態の時にキャッシュに保存されます。

![prefetchされたデータがprefetch cacheから使われている](//images.ctfassets.net/nwdyo7rxqvgg/2wWLOhhZ4dCnUdnSqqcfSU/e95b0694d495efe54ed6e09acfbd3ed4/prefetch-cache.gif) 
*prefetch cacheから必要なファイルが取得されている*

ボタンをクリックすると`vandors-node_modules_mo...`と`src_showTheTime_js-node...`がキャッシュから取得されているのがわかります。

__consoleのDisabed cacheのチェックを外さないとcacheに保存されません！当たり前ですけど１日つまずきました...__

## React.lazy & React.Suspense
どちらもバージョン16.6からの新機能です。

__React.lazy__はReactコンポーネントをdynamic importすることができます。

__React.suspense__は全ての必要なモジュールがフェッチできるまでレンダーを一時停止します。さらにフェッチしている間にfallbackUIを表示する事ができます

この二つはしばしば一緒に使われます。

次の例をみてみましょう。

これはボタンをクリックすると日付が表示されるアプリです。

```markdown:ディレクトリ構造
📦suspense-and-lazyload
 ┣ 📂node_modules
 ┣ 📂src
 ┃ ┣ 📜App.jsx
 ┃ ┣ 📜Now.jsx
 ┃ ┗ 📜index.js
 ┣ 📜.babelrc
 ┣ 📜.gitignore
 ┣ 📜package-lock.json
 ┣ 📜package.json
 ┗ 📜webpack.config.js
```

<br/>

```javaScript:App.jsx
import React, { lazy, Suspense, useState } from 'react';

const Now = lazy(() => import('./Now'));

const App = () => {
  const [open, setOpen] = useState(false);
  const handleClick = () => {
    setOpen(!open);
  };
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <button onClick={handleClick}>Click</button>
      {open && <Now />}
    </Suspense>
  );
};

export default App;
```

`<Now/>`がdynamic importされていて、`<Suspense/>`コンポーネントによってラップされています。

fallbackパラメータに渡された`<div>Loading...</div>`がコンポーネントのローディング中に表示されます。

ではbuildして生成されたファイルをみてみましょう。

```markdown:ディレクトリ構造
📦suspense-and-lazyload-master
 ┣ 📂node_modules
 ┣ 📂dist
 ┃ ┣ 📜index.html
 ┃ ┣ 📜main.js
 ┃ ┣ 📜src_Now_jsx-node_modules_moment_locale_...
 ┃ ┗ 📜vendors-node_modules_moment_locale_af_js-...
 ┣ 📂src
 ┃ ┣ 📜App.jsx
 ┃ ┣ 📜Now.jsx
 ┃ ┗ 📜index.js
 ┣ 📜.babelrc
 ┣ 📜.gitignore
 ┣ 📜package-lock.json
 ┣ 📜package.json
 ┗ 📜webpack.config.js
```

dist以下に `src_Now_jsx-node_modules_moment_locale_...`と`vendors-node_modules_moment_locale_af_js-...`が生成され、コードが分割されているのが確認できます。

サイズを比較してみます。

```markdown:ファイルサイズ比較
//通常のimportの場合
main.js
-----
Stat size:1.73MB
Parsed size:1.86MB
Gzipped size:369.49KB

//reac.lazyの場合
main.js
-----
Stat size:1.07MB
Parsed size:1.15MB
Gzipped size:269.39KB
```

__100.1KB__削減する事ができました。

__ちなみに、これらの機能はサーバーサイドレンダリングで使う事ができません。__<br/>
今後のアップデートで実装される可能はありますが、サーバーサイドで使いたい場合は、[ loadable-components](https://github.com/gregberge/loadable-components " loadable-components")を使うようにしましょう。

### ルートによるコード分割

react-routerとreact.suspneseとreact.lazyを使うことで、ルートレベルでコードを分割することができます。

次の例をみてみましょう

 ```javaScript:App.jsx
import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const About = lazy(() => import('./About'));
const Home = lazy(() => import('./Home'));

const App = () => {
  return (
    <Router>
      <Suspense fallback={<p>Loading...</p>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
        </Switch>
      </Suspense>
    </Router>
  );
};

export default App;
```

HomeコンポーネントとAboutコンポーネントがdynamic importされていて、”/”と”/about”ルートが`<Suspense/>`コンポーネントによってラップされています。

二つのコンポーネントがローディングされる間、fallbackに渡した`<p>Loading...</p>`が表示されます。

ルートごとにファイルが生成されているか確認してみましょう。

```markdown:ファイルの確認
📦suspense-and-lazyload-with-react-router
 ┣ 📂node_modules
 ┣ 📂dist
 ┃ ┣ 📜index.html
 ┃ ┣ 📜main.js
 ┃ ┣ 📜src_About_jsx.js
 ┃ ┗ 📜src_Home_jsx.js
 ┣ 📂src
 ┃ ┣ 📜About.jsx
 ┃ ┣ 📜App.jsx
 ┃ ┣ 📜Home.jsx
 ┃ ┗ 📜index.js
 ┣ 📜.babelrc
 ┣ 📜.gitignore
 ┣ 📜package-lock.json
 ┣ 📜package.json
 ┗ 📜webpack.config.js
```

dist以下にコンポーネントの相対パスの名前が入ったファイルが生成されているのが確認できました。

ルートによってファイルが取得されているかみてみましょう。

![ページ遷移時にそのページに必要なファイルをフェッチする](//images.ctfassets.net/nwdyo7rxqvgg/2KyAjoQZ1Fi09pJluiB8ey/e433c0180e1986a88aa64aae6be76926/reactlazy-suspense-reactrouter.gif)

初回ロード時に、`src_Home_jsx.js`が取得されているのが確認できます。これはHomeルートが”/”であるためです。

Aboutページへのリンクをクリックすると、`src_About_jsx.js`が正常に取得されています。

## まとめ
Googleはページの読み込み速度をランキング要素にしているみたいなので、SEO的にもどんどん分割しようと思いました。

終わり。

## リソース

 - [大規模なWebの開発をする際に気をつけること](https://note.com/konojunya/n/n579593550226 "大規模なWebの開発をする際に気をつけること")
 - [Speed up your React app with dynamic imports and route-centric code splitting](https://blog.logrocket.com/speed-up-react-app-dynamic-imports-route-centric-code-splitting/ "Speed up your React app with dynamic imports and route-centric code splitting")
 - [webpack のコード分割の初歩](https://numb86-tech.hatenablog.com/entry/2020/12/01/093704 "webpack のコード分割の初歩")
 - [Dynamic Imports - webpack](https://webpack.js.org/guides/code-splitting/#dynamic-imports "Dynamic Imports")
 - [Code-Splitting - React](https://reactjs.org/docs/code-splitting.html "Code-Splitting - React")