webpackとesbuild-loaderで爆速なReact開発環境を構築しよう

# はじめに
フロントエンドのライブラリ・フレームワークの移り変わりって激しいですよね。
<a href="https://github.com/webpack/webpack">webpack5</a> がリリースされてもう半年と少しくらい経ってしまいました。
最近では新たなフロントエンドのビルドツールに、以前 <a href="https://qiita.com/olt/items/3e3840db050414cde0ed">こちらの記事</a> に書きました、<a href="https://github.com/evanw/esbuild">esbuild</a> や <a href="https://github.com/vitejs/vite">vite</a> などのビルドツールもますます盛り上がって来ています。

そういう中、webpack が今後どうなっていくのか少し不安というのがあります。今回は、esbuild から派生した、`esbuild-loader` を利用することで、webpack でも爆速なビルド環境が構築できるよということを書きますので、そんな不安を少しでも取り除ければいいなと思います。

## 記事の対象
Node.jsについてある程度理解している、npmなどの環境構築が問題なくできる, webpackの環境構築やなにかしらフロントエンドの環境構築をしたことがある方を対象としています。

## 準備
以下、必要なモジュールなどをインストールしていきます。

```sh
npm i -D typescript ts-node @types/node webpack-cli webpack @types/webpack
```

これで webpack のコンフィグファイルが TypeScript で記述されてても起動できるようになりました。

以下で必要な loader などをインストールしていきます。

```sh
npm i -D esbuild-loader fork-ts-checker-webpack-plugin html-webpack-plugin mini-css-extract-plugin @types/mini-css-extract-plugin postcss-loader sass-loader thread-loader
```
今回重要なのは、<a href="https://github.com/privatenumber/esbuild-loader">esbuild-loader</a>, <a href="https://github.com/TypeStrong/fork-ts-checker-webpack-plugin">fork-ts-checker-webpack-plugin</a>, <a href="https://github.com/webpack-contrib/thread-loader">thread-loader</a> ですので、他は蛇足に近いです。

## <a href="https://github.com/privatenumber/esbuild-loader">esbuild-loader</a>
先ほども説明しましたが、esbuild が提供しているAPIを利用して作成された、loader です。抽象構文木解析, 走査周りの処理は、esbuild と同じものなので、よく利用されている ts-loader よりは確実に早いと思います。
esbuildのビルド速度については<a href="https://github.com/evanw/esbuild">esbuildのドキュメント</a>に記載されています。

## <a href="https://github.com/TypeStrong/fork-ts-checker-webpack-plugin">fork-ts-checker-webpack-plugin</a>
webpackを利用する際に、TypeScriptのトランスパイルによく利用されている、ts-loader の型チェックの部分のみを切り出したプラグインです。このプラグインを利用することで、
esbuild, esbuild-loader の弱点である、<b>型チェックが行えないという問題</b>を解決することができます。

## <a href="https://github.com/webpack-contrib/thread-loader">thread-loader</a>
基本的に、webpackを利用する際には、ファイルの拡張子ごとに loader を分けるなど、細かい設定をすることが多々あります。その際に、この thread-loader を使うことによって、各々の拡張子に対応する loader ごとにスレッドを分割し、マルチスレッドで処理を行うことできます。

thread-loader に関しては、webpack の公式ドキュメントでも紹介されています。
https://webpack.js.org/loaders/thread-loader/


## バンドル設定について
今回は例として、`React + TypeScript + sass` の基本的なバンドル設定を想定して行います。各モジュールのバージョンについては以下の通りです。（2021/04/24時点）

```package.json
{
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^5.2.0"
  },
  "devDependencies": {
    "@types/mini-css-extract-plugin": "^1.4.0",
    "@types/node": "^14.14.37",
    "@types/react": "^17.0.3",
    "@types/react-dom": "^17.0.3",
    "@types/react-router-dom": "^5.1.7",
    "@types/webpack": "^5.0.0",
    "css-loader": "^5.2.0",
    "esbuild-loader": "^2.11.0",
    "fork-ts-checker-webpack-plugin": "^6.2.0",
    "html-webpack-plugin": "^5.3.1",
    "mini-css-extract-plugin": "^1.4.0",
    "postcss-loader": "^5.2.0",
    "sass-loader": "^11.0.1",
    "thread-loader": "^3.0.1",
    "ts-node": "^9.1.1",
    "typescript": "^4.2.3",
    "webpack": "^5.28.0",
    "webpack-cli": "^4.6.0"
  }
}
```

ファイルパスの配置は、以下の通りです。
<img width="200px" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212521/54f49837-bb12-5098-0182-f5e9db7d64f5.png>
</img>

## webpackの設定
以下に webpack の設定ファイルを記述していきます。
わかりづらい箇所は適宜コメントをしています。

```webpack.config.ts
const path = require('path')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = () => {
  const IS_DEV = process.env.NODE_ENV === 'development';

  const configs = {
    // ForkTsCheckerWebpackPluginが自動的にtsconfig.jsonを見つけるために必要
    context: __dirname,
    mode: process.env.NODE_ENV,
    devtool: IS_DEV ? 'eval-sorce-map' : 'hidden-nosources-source-map',
    entry: path.resolve(__dirname, 'src/Main.tsx'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    },
    resolve: {
      modules: [path.resolve(__dirname, 'node_modules')],
      alias: {
        '@': path.resolve(__dirname, './src'),
      },
      extensions: ['.ts', '.tsx', '.d.ts', '.js']
    },
    module: {
      rules: [
        {
          test: [/\.css$/, /\.scss$/, /\.sass$/],
          exclude: /node_modules/,
          use: [
            {
              loader: 'thread-loader',
              options: {
                // worker thread数
                workers: 2,
                // 並列処理の上限数
                workerParallelJobs: 40,
                // キャッシュのmaxサイズ
                workerNodeArgs: ['--max-old-space-size=512'],
                // thread pool名（固有）
                name: 'css-loader-pool',
              }
            },
            { loader: MiniCssExtractPlugin.loader },
            { loader: 'postcss-loader' },
            { loader: 'sass-loader' }
          ],
        },
        {
          test: /\.ts$/,
          exclude: /node_modules/,
          use: [
            {
              loader: 'thread-loader',
              options: {
                workers: 2,
                workerParallelJobs: 80,
                workerNodeArgs: ['--max-old-space-size=512'],
                name: 'ts-loader-pool',
              }
            },
            {
              loader: 'esbuild-loader',
              options: {
                loader: 'ts',
                minify: !IS_DEV,
                target: 'es2015',
              },
            }
          ],
        },
        {
          test: /\.tsx$/,
          exclude: /node_modules/,
          use: [
            {
              loader: 'thread-loader',
              options: {
                workers: 2,
                workerParallelJobs: 80,
                workerNodeArgs: ['--max-old-space-size=512'],
                name: 'tsx-loader-pool',
              }
            },
            {
              loader: 'esbuild-loader',
              options: {
                // loaderはstring型なのでtsとtsxファイルの設定を別々に定義する必要がある
                loader: 'tsx',
                minify: !IS_DEV,
                target: 'es2015',
              },
            }
          ],
        },
      ],
    },
    plugins: [
      new ForkTsCheckerWebpackPlugin(),
      new HtmlWebpackPlugin({
        filename: 'index.html',
        template: 'src/html/index.html',
      }),
      new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[id].css',
      }),
    ],
  }

  // webpack5以降からはdevtoolsを個別に後から設定する必要がある
  if (IS_DEV) {
    configs.devtool = 'eval-source-map';
  }

  return configs;
}
```

npmスクリプトも以下のように設定することによって、便利になりそうですね。

```package.json
{
  "scripts": {
    "watch": "NODE_ENV=\"development\" webpack --mode development --watch --hot",
    "build-dev": "NODE_ENV=\"development\" && webpack --mode development",
    "build-prod": "NODE_ENV=\"production\" && webpack --mode production"
  },
}
```

# おわりに
今回は、大まかに esbuild-loader を利用した webpack のフロントエンドの環境構築について書きました。みなさんのお役に立つと幸いです。
なにか質問やご指摘あれば、コメントの方によろしくお願いします。