maesblog

create-react-appで使えるdynamic import()でReactのComponentを動的に読み込む(Code Splitting)

2017年5月19日に「create-react-app v1.0.0」がリリースされました。追加された機能の1つの中で、dynamic import()への対応というものがありました。dynamic import()は読んで字の如く動的にモジュールを読み込むための機能です。ECMAScript的には、2017年6月現在TC39にてProposalとして仕様策定が進められている段階の機能です。ただ、create-react-appでは、その機能を一早く利用できるようになったので(TypeScriptでもv2.4から対応しました)、その使用方法を紹介していきます。通常最初にファイルを読み込んでおく必要があるCSR(クライアントサイドレンダリング)において、必要に応じてファイルを読み込めるようになるので、初回ロード時間を短縮し、パフォーマンスの向上に役立ちます。ぜひお試しください。

はじめに – create-react-appとは

昨今のフロントエンドの開発において、開発環境の構築が複雑化してきています。Reactの開発に関しても例外ではありません。そこでReactの開発を先導しているFacebookから提供されているのが、「create-react-app」です。create-react-appは、コマンド1つでReactの開発環境を構築してくれる便利なツールです。

create-react-appについては、当ブログでもリリース当初に記事を書いています。若干内容が古いかもしれませんが、使用方法など大きく変わっていませんので、参考程度に合わせて見てもらえればと思います。

create-react-appは、簡単にReactの開発環境を構築してくれるツールですが、さらに以下のような最新のJavaScript標準のスーパーセットにも対応しています

今回紹介するdynamic import()も上記の中に含まれている機能の1つです。dynamic import()は、2017年6月現在、上記の通りTC39のproposalのstage 3(候補版)の段階にある機能です。このような最新の機能を使用できるのもcreate-react-appの嬉しいポイントです。

ES2015の静的import宣言

まず、dynamic import()の説明に行く前に、ES2015の静的import宣言について振り返ってみましょう。

ES2015のimportは静的です。以下のようにトップレベルで宣言することで、コンパイル時にインポートするファイルを指定します。実行時に変更することはできません。

import * as module from './module.js';

以下のような特徴があります。

  • importの宣言はモジュールのトップレベルで行います。したがってif文やイベントハンドラーの内部でモジュールを読み込むようなことができなくなっています。
  • importするモジュールは一度指定したら、(関数の呼び出しなどによって)実行時に変更できないようになっています。

こうした静的であるということによって、バンドル時にデッドコードを除去することができたり、モジュールをコンパクトに効率よくバンドルすることができたり、高速にインポートできたりするなど様々な利点があります。静的import宣言の利点についての詳細は以下をご参照ください。

dynamic import() – 動的インポート

一方で、ユーザーの言語など実行時にしかわからない要因に対応するためだったり、パフォーマンスを考慮してそのモジュールが必要となるまでロードしないようにするためだったり、ノンクリティカルなモジュールのロードに失敗した状態を保持しないように堅牢性を高めるためだったり、動的にモジュールを読み込む術が求められているという理由で、ここにきてdynamic import()の策定が進められるようになったとのことです。

dynamic import()の使い方

まず、読み込みたいモジュールとして以下のようなモージュルがあったとします。

export default class Module {
  foo() {
    return 'foo';
  }
}
module.js

上記のmodule.jsdaynamic import()を使って動的にインポートする場合は、以下のように記述します。

// module.jsをインポートする
import('./module').then(
  // defaultとして読み込んだモジュールを「MyModule」という名前空間として、
  // then()のコールバック関数の引数に渡す
  ({ default: MyModule }) => {
    console.log(new MyModule().foo()); // => 'foo'
  }
).catch(err => {
  //  エラー処理
});

dynamic import()は演算子という扱いですが、関数のように振る舞います。

  • 引数には、静的インポートの宣言時と同じ文字列のフォーマットでモジュールを指定することができます。さらに、最終的に文字列に変換されるような「任意の式」で指定することもできます。
    import(`module_${getModuleName()}.js`)
    .then(···);
  • dynamic import()は読み込むモジュールに名前空間を与えた状態でPromiseを返します。Promiseは、モジュールがfetchされ、インスタンス化され、全ての依存関係が解決された後に作られます。読み込まれたモジュールは、Promiseオブジェクトのthen()メソッドのコールバック関数やasync/awaitを通して扱うことになります

以下の記事は、dynamic import()についてわかりやすく説明されています。私も参考にしました。紹介しておきます。

Reactでdynamic import()を使う

dynamic import()の使い方を簡単に紹介しましたので、早速Reactでdynamic import()を使って、動的にコンポーネントを読み込む方法を紹介します。create-react-appで作成したプロジェクトであれば、特に設定など何もせずにdynamic import()を使うことができるようになっています

読み込むコンポーネントの準備

まずは読み込むコンポーネントを出力するモジュールを用意します。今回は、以下のComponent1.jsComponent2.jsComponent3.jsという3つのファイルを用意しました。それぞれ単純なFunctional Component<div>コンポーネント名</div>を返すだけのコンポーネントになっています。

import React from 'react';

const Component1 = () => {
  return (
    <div>Component1</div>
  );
}

export default Component1;
Component1.js
import React from 'react';

const Component2 = () => {
  return (
    <div>Component2</div>
  );
}

export default Component1;
Component2.js
import React from 'react';

const Component3 = () => {
  return (
    <div>Component3</div>
  );
}

export default Component3;
Component3.js

ディレクトリ構成は以下の通りです。srcディレクトリ内に作成した3つコンポーネントを置きました(説明に必要のないディレクトリやファイルは省略しています)。

my-react-app
┗ src
   ┣━ App.js
   ┣━ Component1.js
   ┣━ Component2.js
   ┣━ Component3.js
   ┗━ index.js

静的importで読み込む場合

比較のために、通常の静的import宣言でコンポーネントを読み込む場合の書き方について紹介しておきます。この書き方は言わずもがなですね。必要なコンポーネントを予め冒頭で宣言しているので、実行時に変更することはできません

import React from 'react';
// importで読み込みたいコンポーネントを宣言する
import Component1 from './Component1';
import Component2 from './Component2';
import Component3 from './Component3';

class App extends React.Component {
  render () {
    return (
      <div>
        {/* JSXのタグとして読み込んだコンポーネントを紐づける */}
        <Component1 />
        <Component2 />
        <Component3 />
      </div>
    );
  }
}

export default App;
App.js

dynamic import()で読み込む場合

それでは、dynamic import()を使って動的にコンポーネントを読み込む場合はどのような書き方になるでしょうか。以下は、上記の静的import宣言を使ったコードを、dynamic import()を使って動的にコンポーネントを読み込むように書き換えたものとなります。

import React from 'react';

class App extends React.Component {
  constructor() {
    super();
    // 読み込むコンポーネントをthis.stateに定義する
    this.state = {
      component1: null,
      component2: null,
      component3: null,
    };
  }
  async componentDidMount() {
    // dynamic import()を使ってコンポーネントを動的に読み込む
    const { default: Component1 } = await import('./Component1');
    const { default: Component2 } = await import('./Component2');
    const { default: Component3 } = await import('./Component3');
    // 読み込んだコンポーネントでthis.stateの値を更新する
    this.setState({
      component1: Component1(),
      component2: Component2(),
      component3: Component3(),
    });
  }
  render () {
    return (
      {/* 読み込んだコンポーネントをDOMに反映する */}
      <div>
        { this.state.component1 || <div>...</div> }
        { this.state.component2 || <div>...</div> }
        { this.state.component3 || <div>...</div> }
      </div>
    );
  }
}

export default App;
Component3.js

ここでのポイントは以下の通りです。

  • 読み込むコンポーネントをthis.stateに定義する
  • componentDidMount()メソッド内でdynamic import()を呼び出し、コンポーネントを読み込む
  • 読み込んだコンポーネントを、setState()メソッドを使ってstateに反映し、再レンダリングすることでDOMに反映させる

ポイントは、dynamic import()をいつのタイミングで呼び出すかというところでしょうか。今回の例では、ReactのライフサイクルメソッドcomponentDidMount()メソッドを使って、Appコンポーネントがマウントされたタイミングでdynamic import()を呼び出すようにしています。ここでコンポーネントを読み込む部分をES2017のasync/awaitで書くと、すっきり書くことができます。

ES2017のasync/awaitについては、MDNや当ブログの記事などをご参照ください。

dynamic import()で読み込んだコンポーネントをDOMに反映させる術として、setState()メソッドを呼び出すようにします。したがって、予めAppコンポーネント内のthis.stateに、読み込むコンポーネントを定義しておきます。

Reactでdynamic import()を使ってコンポーネントを読み込む基本的な流れは以上の通りとなります。なんとなくおわかりいただけましたでしょうか。

Promise.all()で書く場合

なお、上記のコードのcomponentDidMount()メソッド内のawaitの部分ですが、Promise.all()メソッドを使って以下のように書くこともできます。状況に応じて、書き方を使い分けるとよいでしょう。

async componentDidMount() {
  const [Component1, Component2, Component3] = await Promise.all([
    import('./Component1'),
    import('./Component2'),
    import('./Component3'),
  ]);
  this.setState({
    component1: Component1.default(),
    component2: Component2.default(),
    component3: Component3.default(),
  });
}
Promise.all()で書く場合

ユーザーアクションに応じてコンポーネントを読み込む

読み込むコンポーネントによっては、最初から必要ではないものもあります。ユーザーのアクションに応じて読み込みたい場合もある以下と思います。dynamic import()は、イベントに応じてコンポーネントを読み込むようにすることも可能です。

例えば、上記のApp.jsファイルのコードを以下のように修正してみます。ユーザーが押せるボタンとそのボタンが押された時のイベントを受け取るイベントハンドラー、コールバック関数を追加しました。

import React from 'react';

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      component1: null,
      component2: null,
      component3: null,
    };
    this.handleClick = this.handleClick.bind(this);
  }
  async componentWillMount() {
    // 省略...
  }
  handleClick(e) {
    e.preventDefault();
    import('./Component1')
    .then((module) => {
      return this.setState({
        component3: module.default(),
      });
    })
    .catch((e) => {
      return console.log(e);
    });
  }
  render () {
    return (
      <div>
        { this.state.component1 || <div>...</div> }
        { this.state.component2 || <div>...</div> }
        { this.state.component3 || <div>...</div> }
        <button onClick={this.handleClick}>コンポーネント追加</button>
      </div>
    );
  }
}
App.js

新たに追加したコードにより、ボタンが押されたら、dynamic import()により、Component1が動的に読み込まれ、this.state.component3の値としてsetState()メソッドを呼び出し、Component3が表示されていた箇所にComponent1が表示されるようになっています。

製品版としてビルドする

create-react-appには製品版としてビルドする機能が用意されています。以下のコマンドで製品版としてビルドすることができます。

$ npm run build

npm run buildコマンドが実行されると、buildディレクトリに製品版のアプリケーションが最適化された状態でビルドされます。ファイルは圧縮され、ファイル名にはハッシュが含まれるようになります。

上記のコードをビルドすると、以下のファイルがbuildディレクトリに作られます(説明に必要のないディレクトリやファイルは省略しています)。

build/
┣ static/
┃   ┗ js/
┃      ┣ 0.67d52ea3.chunk.js
┃      ┣ 1.cdb9bb27.chunk.js
┃      ┣ 2.5bad8e50.chunk.js
┃      ┗ main.114e7418.js
┣ index.html
┣ manifest.json
┗ service-worker.js

dynamic import()によって、動的に読み込まれるファイルは、chunkファイルとしてビルドされます。chunkファイルを使ったCode Splittingに関する詳細は以下のwebpackのドキュメントをご参照ください。

なお、create-react-appによってファイル名にハッシュがつけられていることもわかるかと思います。製品版にはデフォルトでService Workerが含まれており、ローカルキャッシュされるようになっています。また、オフライン表示にも対応するようになっています。

まとめ

以上、Reactでdynamic import()を使って、動的にコンポーネントを読み込む方法を紹介してきました。基本的にはPromiseを扱う感覚で使用できるかと思います。用途としては、例えば、タブによって表示させるコンポーネントを切り替える時や、react-routerを使ったルーティングにより表示させるコンポーネントを切り替える時などが考えられます。うまく使えばWebアプリの高速化に貢献できるのではないでしょうか。

create-react-appでは、今回紹介したdynamic import()が使えたり、機能が大変充実しています。他にも新しい技術としては、PWA(Progressive Web Apps)Service Workerにも対応していたり、かなり使えるツールになっています。この辺りの技術の使い方もそのうち紹介していきたいと思います。

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門
  • 『Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門』
  • 著者: Stoyan Stefanov, 牧野聡(翻訳)
  • 出版社: オライリージャパン
  • 発売日: 2017年3月11日

関連記事

コメント

  • 必須

コメント