Webpack のコード分割機能は、コードを異なるバンドルに分離し、必要に応じてこれらのファイルをロードまたは並行してロードすることができます。
コード分割とは#
私たちは、webpack を使用して react アプリケーションをパッケージ化する際、webpack がアプリ全体を 1 つの js ファイルにパッケージ化することを知っています。ユーザーが初回画面にアクセスすると、全体の js ファイルが一度に読み込まれるため、初回画面のレンダリング速度が遅くなる問題が発生します。そこで、webpack はコード分割の機能を開発しました。この機能は、コードを異なるバンドルに分離し、必要に応じてこれらのファイルをロードまたは並行してロードすることができます。コード分割は、より小さなバンドルを取得し、リソースのロード優先度を制御するために使用でき、適切に使用すれば、ロード時間に大きな影響を与えることができます。
一般的なコード分割の方法は 3 つあります:
- エントリーポイント:entry 設定を使用して手動でコードを分離します。
- 重複防止:CommonsChunkPlugin を使用してチャンクを重複させて分離します。
- 動的インポート:モジュールのインライン関数呼び出しを通じてコードを分離します。
この記事では、動的インポートの方法のみを議論します。
動的インポート#
動的コード分割に関して、webpack は 2 つの類似した技術を提供しています。動的インポートに対して、最初の方法であり、優先される方法は、ECMAScript 提案に準拠した import () 構文を使用することです。2 つ目は、webpack 特有の require.ensure を使用することです。まずは、最初の方法を試してみましょう……
注意:import () 呼び出しは内部で promises を使用します。古いバージョンのブラウザで import () を使用する場合は、polyfill ライブラリ(例えば es6-promise や promise-polyfill)を使用して Promise を shim してください。
以下では、react-router 4 を使用して react のコード分割を実現します。
react のコード分割と遅延読み込み#
react-router4 を使用してコード分割を行う際、コミュニティには react-loadable のような成熟したサードパーティライブラリが実装されています。ここでは、サードパーティライブラリを使用せずにコード分割を実現する方法を紹介します。
ここでは、あなたが react、react-router4、webpack について基本的な理解を持ち、簡単な開発環境を構築できることを前提とします。まず、私が構築したプロジェクトのエントリーファイルを見てみましょう。
src 下 index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('app'));
src 下 App.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import Routes from './routes';
export default () => (
<BrowserRouter>
<Switch>
{
Routes.map(({name, path, exact = true, component }) => (
<Route path={path} exact={exact} component={component} key={name} />
))
}
</Switch>
</BrowserRouter>
)
App.js では、react-router-dom を使用してルーティングを行い、routes.js ファイルをルーティング設定ファイルとして使用しています。次に、ルーティング設定を使用してルーティングコンポーネントを生成します。次に、ルーティング設定 routes.js を見てみましょう。
import AsyncComponent from './components/acync-component';
export default [
{
name: 'ホーム',
icon: 'home',
path: '/',
component: AsyncComponent(() => import('./containers/home'))
},
{
name: '詳細',
icon: 'detail',
path: '/detail',
component: AsyncComponent(() => import('./containers/detail'))
}
]
routes.js では、ルーティングコンポーネントに必要なパラメータを設定しています。ルーティングパラメータでは、非同期コンポーネント AsyncComponent を使用していることに注意してください。ここではコンポーネントをインポートしていないのではなく、AsyncComponent に関数を渡しています。この関数は、*AsyncComponent(() => import ('./containers/home'))* コンポーネントが作成されるときに動的にインポートされます。同時に、関数を引数として渡すこの書き方は、webpack にここでコード分割が必要であることを認識させることができます。
import () を使用するには、Babel プリプロセッサーと Syntax Dynamic Import Babel Plugin のようなものを使用する必要があります。import () は promise を返すため、async 関数と一緒に使用できます。async 関数を使用するには、babel-plugin-transform-runtime を使用する必要があります。
babel プラグインをインストール:
npm i -D babel-plugin-syntax-dynamic-import babel-plugin-transform-runtime
本プロジェクトで使用している他の babel プラグインには、babel-core、babel-loader、babel-preset-env、babel-preset-react などがあり、主に jsx コンパイルに使用されます。
.babelrc を設定
ルートディレクトリに.babelrc を新規作成し、以下のように設定します:
{
"presets":["react","env"],
"plugins": ["syntax-dynamic-import", "transform-runtime"]
}
非同期コンポーネント AsyncComponent#
本プロジェクトの AsyncComponent は src/components/async-component/index.js にあり、コードは以下の通りです:
import React, { Component } from 'react';
export default (loadComponent, placeholder = '読み込み中。。。。') => {
return class AsyncComponent extends Component {
constructor() {
super();
this.state = {
Child: null
}
this.unmount = false;
}
componentWillUnmount() {
this.unmount = true;
}
async componentDidMount() {
const { default: Child } = await loadComponent();
if (this.unmount) return;
this.setState({
Child
})
}
render() {
const { Child } = this.state;
return (
Child ? <Child {...this.props} /> : placeholder
)
}
}
}
まず、これは高階コンポーネントであり、新しいコンポーネントを返します。2 つの引数を受け取ります。1 つは動的に読み込む必要があるコンポーネントのメソッド、もう 1 つは動的読み込み時のプレースホルダーで、Loading コンポーネントを渡すこともできます。
返された AsyncComponent 内部では、constructor で state を Child として初期化し、値を null に設定し、this.unmount = false を定義して、コンポーネントがアンマウントされたかどうかを示します。
async を使用して非同期メソッドを定義し、componentDidMount で、await を使用して渡された最初の引数を非同期に実行し、現在のルートのコンポーネントを動的に読み込みます。
注意:
ES6 モジュールの import () メソッドを呼び出す際は、モジュールの.default 値を指す必要があります。これが promise が処理された後に返される実際の module オブジェクトだからです。
したがって、ここでは ES6 のオブジェクト分割を使用してモジュールの default を取得し、Child に割り当てます。
次に、コンポーネントがアンマウントされた状態を確認し、アンマウントされている場合は戻ります。
次に、Child を state に設定します。
render メソッドでは、state から Child を取得し、三項演算子を使用して Child が true かどうかを判断します。true の場合は Child コンポーネントをレンダリングし、this.props を注入します。そうでない場合はプレースホルダーをレンダリングします。
コンポーネントが componentWillUnmount されると、this.unmount を true に設定します。
テスト検証#
containers 内に home と detail という 2 つのフォルダーを新規作成し、それぞれのフォルダー内に index.js を作成して 2 つのルートコンポーネントを作成します。コードは以下の通りです:
home/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router-dom';
import '../../style.css';
import icon from '../../assets/404.png';
export default class Home extends React.Component {
render() {
return (
<div>
<h1>hello webpack</h1>
<img src={icon} alt=""/>
<Link to="/detail">詳細</Link>
</div>
)
}
}
detail/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import '../../style.css';
import icon from '../../assets/404.png';
export default class Detail extends React.Component {
render() {
return (
<div>
<h1>hello webpack detail</h1>
<img src={icon} alt=""/>
</div>
)
}
}
ルートディレクトリの package.json に起動スクリプトを設定:
"scripts": {
"start": "webpack-dev-server --mode development",
"build": "webpack --mode production"
},
その後、npm start を実行してプロジェクトを起動します:
ブラウザで localhost:8080 にアクセスします。
ページが最初に main.js と 0.js を読み込んでいるのが確認できます。詳細ボタンをクリックしてhttp://localhost:8080/detail に移動します。
すぐに 1.js が読み込まれ、これによりコード分割が実現され、各ルートが動的に読み込まれます。初回画面のレンダリング速度が向上しました。