💬

Create React AppからVITEへマイグレート

2022/11/27に公開2

経緯

今のプロジェクトは2年前ほどから始まっていて、Create React Appが使用されています。毎回devサーバーを起動するときに2-3分がかかってしまい、結構困っています。それでVITEとかでどうかなと思ってやってみました。

CRAはwebpackを使っているので、module graphの構築でファイルIO、tansform, parseのステップで時間がかかってしまい、devサーバー起動時にこれらの作業を終わるまで待たなければなりません。プロジェクトが大きくなると、待つ時間も増えていきます。

VITEでは全く違う策略をとっていて、起動する前に一回全てのファイルに対してtransform, parseなどする操作がありません。代わりに、ブラウザーからリクエストが届いたときに、ミドルウェアでこれらの仕事を行うらしい。そのため、devサーバーの起動は非常に速くなります。

どのくらい早くなるかを期待しながら、まずはマイグレーションをやっていこうと。

マイグレートステップ

ライブラリー

まずは必要なライブラリーをインストールします。

npm install -D vite @vitejs/plugin-react vite-tsconfig-paths
# or 
yarn add -D vite @vitejs/plugin-react vite-tsconfig-paths

ts使っているかによって最後のものは適宜削除しますが、あればtsでのパス解析ができますので、ts使っている場合はおすすめです。例えばsrcフォルダーからの相対パスが分かるので、インポートするときに、src/components/Menu/Navフォルダーをcomponents/Menu/Navに書けます。

CRAは使わないので、関連パッケージを削除します。

npm uninstall react-scripts
#or 
yarn remove react-scripts

index.htmlファイル

Reactデフォルトでは、このファイルをpublicフォルダーに置いていますが、VITEを使う場合、こちらのファイルをルートフォルダーに移動する必要があります。なお、%PUBLIC_URL%といったプレースホルダーも不要になるため、削除で良いです。

<!-- これを -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />

<!-- こちらに -->
<link rel="icon" href=".public/favicon.ico" />

上記は一例ですが、他にプレースホルダーがある箇所も全部入れ替えます。

次にjsxの入り口ファイルをインポートします。

<div id="root">...</div>
<!-- インデックスjsx/tsxファイルを追加 -->
<script type="module" src="/src/index.tsx"></script>

スクリプト

CRAのスクリプトでサーバー起動、ビルドしていますが、今はVITE用のものに入れ替えます。

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

から次のように

"scripts": {
  "start": "vite",
  "build": "tsc && vite build",
  "serve": "vite preview"
},

vite previewではプロダクションビルドが実行されるので、例えば、tsconfigで設定されたoutdirのファイルが対象となります。viteだけであればdevサーバーが立ち上げられます。ポート番号などの設定について、次の節で詳しくみてみたいと思います。

ts関連アップデート

tsを使う場合、いくつか変更点があります。まずvite関連のタイプをtsconfigに追加します。

{
  "compilerOptions": {
    "types": ["vite/client"],
  }
}

次にタイプ宣言ファイルを作ります。これはsrcフォルダーもしくは、src/typesフォルダーといったタイプ管理しているところにおけば良いです。

// vite-env.d.ts
/// <reference types="vite/client" />

代わりに既存のreact-app-env.d.tsは削除でOKです。

コンフィグファイル

以上のステップが終わったら、次にコンフィグファイルを作成します。プロジェクトのルートフォルダーに、touch vite.config.tsを実行します。とりあえず次に下記のコードを追加し、用途によって徐々に増やしていきます。

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), viteTsconfigPaths()],
});

環境変数

VITEのプロジェクトで環境変数の扱いがだいぶ違います。2つのステップがありますが、まずREACT_APP_で始まるものを、VITE_に入れ替えて、次にprocess.envimport.meta.envへ変更します。

ただし、REACT_APPのプリフィックスをそのままにしたい場合は一応可能です。

まずは一つプラグインを追加:

npm install -D vite-plugin-env-compatible
# or
yarn add -D vite-plugin-env-compatible

次にconfigファイルを更新

import env from 'vite-plugin-env-compatible'

export default defineConfig({
  envPrefix: 'REACT_APP_',
  plugins: [react(), viteTsconfigPaths(), env()],
});

DEV環境

開発環境の設定は、server項目にあります。

export default defineConfig({
  plugins: [react(), viteTsconfigPaths(), env()],
  server: {
    host: 'localhost',
    port: 3000,
  },
});

他の設定はこちらに参照。

Docker使用時のホットリロード

VITEにはホットリロード機能がありますが、ファイルに変更がある場合、変更箇所のファイルを更新して、rerenderなどを行なってくれます。これのおかげで開発がだいぶ快適になりますが、dockerを利用するときに少し設定が必要です。

  server: {
    open: true,
    host: '0.0.0.0',
    port: 3000, // docker container port
    hmr: {
      port: 3000, // host machine port mapping
    },
    watch: {
      usePolling: true,
    },
  },

プロダクションビルド

プロダクションビルドするときに、buildという項目で変更します。

  build: {
    outDir: 'build',
    rollupOptions:{
      plugins: []
    }
  },

NodejsのPolyfill

例えば、nodejs環境依存のAPI、Buffer, processとかがコード内またはインストールされたライブラリーに利用されるケースがあります。これはCRAには問題なかったのですが、VITEではデフォルトのポリフィルがないので、何もしないとコンパイルされたコードでは、Buffer is undefinedなどのエラーが出て、アプリが起動できなくなります。

FE側にもnodejsのAPIを使うようにするために、関連するポリフィルのライブラリーなどをDEV環境とプロダクション両方に追加します。

import GlobalsPolyfills from '@esbuild-plugins/node-globals-polyfill';
import rollupNodePolyFill from 'rollup-plugin-node-polyfills';

//...
  build: {
    outDir: 'build',
    rollupOptions:{
      plugins: [rollupNodePolyFill()]
    }
  },
  optimizeDeps: {
    esbuildOptions: {
      define: {
        global: 'globalThis',
      },
      plugins: [GlobalsPolyfills({ process: true, buffer: true })],
    },
  },

@xxxでインポートしたい

普段はvite-tsconfig-pathsがあるから、tsconfigで設定されたソースフォルダーからインポートするのが多いのですが、一応より広い用途があるパス解析の設定が可能です。

import { resolve } from 'path';
// ...
  resolve: {
    alias: {
      '@': resolve(__dirname, '/src/types'),
    },
  },

最後に

これでようやく完成:

import { resolve } from 'path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsconfigPaths from 'vite-tsconfig-paths';
import GlobalsPolyfills from '@esbuild-plugins/node-globals-polyfill';
import rollupNodePolyFill from 'rollup-plugin-node-polyfills';

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, '/src'),
    },
  },
  plugins: [react(), viteTsconfigPaths()],
  build: {
    outDir: 'build',
    rollupOptions:{
      plugins: [rollupNodePolyFill()]
    }
  },
  server: {
    open: true,
    host: '0.0.0.0',
    port: 3000, // docker container port
    hmr: {
      port: 3000, // host machine port mapping
    },
    watch: {
      usePolling: true,
    },
  },
  optimizeDeps: {
    esbuildOptions: {
      define: {
        global: 'globalThis',
      },
      plugins: [GlobalsPolyfills({ process: true, buffer: true })],
    },
  },
});

今回の導入に置いて、一番予想外の問題はポリフィルのところでした。CRAはすでに揃えているので、気づいていませんでしたが、最初にBuffer is undefinedを見たときに???との気持ちでした。公式のドキュメントにも追加して欲しい内容ですね。

導入後の初回ロード以外、devサーバーの起動、ホットリロードなどが爆速になっているので、CRAよりはだいぶ快適になっています。

ではでは、今回はこれで。

Discussion

tommy34tommy34

CRAでdevサーバーの起動に2-3分かかっていたのがViteを使った結果何秒くらいになったのでしょうか

convers39convers39

コメントありがとうございます!

CRAを使う場合、localhostが立ち上がるまで3分弱かかっていました。VITEでもしlocalhost立ち上がりだけであれば、10秒以内がほとんどですが、初回ロード時に時間がかかるので、結局ページがレンダーリングされるまでに40-60秒はかかります。

ただ、VITEのこの初回ロードはキャッシングされるので、それ以降はdocker compose down -> compose upしても、次回からは10秒程度で画面がレンダーリングされます。CRAの場合は毎回3分弱なのでここはだいぶ差が出てきますね。

しっかりとベンチマークとか測っていないのですが、ご参考になれると嬉しいです。