thread-loaderを使ってwebpackのビルドを早くする

# weebpackのビルドパフォーマンス改善

Node.JS等の開発でよく用いられるwebpackですが、サービスやアプリケーションの規模が大きくなるとともにビルド時間が長くなって行きます。
この問題を解決するために、時間のかかる（expensive）loader の処理を worker pool で実行し、それによって複数スレッドでの並列ビルドを可能にするthread-loaderを使ってみようと思います。

今回は2600行18ファイルあるReact+TypeScriptで開発したアプリケーションのビルド速度を
thread-loaderを使う前と後での初回ビルドの速度の変化を載せたいと思います。

# 実測

## befor
大体10秒くらいかかります。

<img width="433" alt="スクリーンショット 2020-06-09 21.53.43.png (72.9 kB)" src="https://img.esa.io/uploads/production/attachments/13671/2020/06/09/54828/816c85a1-4ffa-4ad0-b54f-6e0737e42a6b.png">

webpack.conf.jsの中身はこんな感じ。

```webpack.conf.js
'use strict'

const path = require('path')
const rm = require('rimraf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Dotenv = require('dotenv-webpack')

/* delete files */
rm.sync(path.join(__dirname, '..', 'prod'))
rm.sync(path.join(__dirname, '..', 'dist'))

module.exports = {
  output: {
    path: path.join(__dirname, '..', 'dist'),
    publicPath: '/'
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
  },

  module: {
    rules: [
      {
        test: [/\.ts$/, /\.tsx$/],
        include: path.resolve('src'),
        use: [
          {
            loader: 'babel-loader'
          },
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
              happyPackMode: true
            }
          },
        ]
      },
      {
        test: [/\.js$/, /\.jsx$/],
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10000,
          }
        }
      },
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './public/index.html',
      inject: true
    }),
    new Dotenv({ systemvars: false }),
  ]
}

```


## after

大体2.5秒くらい早くなりました。

<img width="412.5" alt="スクリーンショット 2020-06-09 1.25.50.png (75.0 kB)" src="https://img.esa.io/uploads/production/attachments/13671/2020/06/09/54828/ea5576a6-b956-4768-843f-6bf77e73d8f1.png">

以下のようにTypeScriptをBabelでトランスパイスするタイミングを並列化すると
高速化が実現できます。

```webpack.conf.js
'use strict'

const path = require('path')
const rm = require('rimraf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Dotenv = require('dotenv-webpack')

/* delete files */
rm.sync(path.join(__dirname, '..', 'prod'))
rm.sync(path.join(__dirname, '..', 'dist'))

module.exports = {
  output: {
    path: path.join(__dirname, '..', 'dist'),
    publicPath: '/'
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
  },

  module: {
    rules: [
      {
        test: [/\.ts$/, /\.tsx$/],
        include: path.resolve('src'),
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1
            }
          },
          {
            loader: 'babel-loader'
          },
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
              happyPackMode: true
            }
          },
        ]
      },
      {
        test: [/\.js$/, /\.jsx$/],
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10000,
          }
        }
      },
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './public/index.html',
      inject: true
    }),
    new Dotenv({ systemvars: false }),
  ]
}
```

# まとめ
Node.JSプログラムやTypeScriptソースをwebpackでビルドするときでも速度は短くなるのでおすすめです。
 ReactのようなSPA開発の場合はwebpack-dev-server等で差分更新させながら開発して、
プロダクションビルドをするのはデプロイする時くらいだとは思いますが、差分更新も
アプリケーションの規模が大きくなればビルドの時間は増えると思うので少しはストレスが軽減するかと思います。
他にもcache-loader等のビルドを高速にするloaderもあるので興味があれば試しみると面白いかもしれません。

## 注意点?

> Each worker is a separate node.js process, which has an overhead of ~600ms. There is also an overhead of inter-process communication.

> Use this loader only for expensive operations!

worker は独立した Node のプロセスなので、起動のオーバーヘッドはそれなりに大きいらしい。
時間のかかる処理にだけ使うように、とのことなのでむやみやたらに使えば良いわけではなく、むしろデグレして遅くなるケースもあるみたいです。

# おまけ

Vueのwebpack.conf.jsのサンプルも置いておきます。

```webpack.conf.js
'use strict'

const path = require('path')
const rm = require('rimraf')
const utils = require('./utils')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const resolve = (dir) => {
  return path.join(__dirname, '..', dir)
}

/* delete files */
rm.sync(resolve('prod'))
rm.sync(resolve('dist'))

module.exports = {
  output: {
    path: resolve('dist'),
    publicPath: '/'
  },
  entry: {
    app: './src/main.ts'
  },
  resolve: {
    extensions: ['.js', '.ts', '.vue', '.json'],
    alias: {
      vue$: 'vue/dist/vue.esm.js',
      '@': resolve('src')
    }
  },

  module: {
    rules: [
      {
        test: /\.ts$/,
        include: path.resolve('src'),
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1
            }
          },
          {
            loader: 'babel-loader'
          },
          {
            loader: 'ts-loader',
            options: {
              appendTsSuffixTo: [/\.vue$/],
              happyPackMode: true
            }
          }
        ]
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          esModule: true,
          cacheBusting: true,
          loaders: {
            // Since sass-loader (weirdly) has SCSS as its default parse mode, we map
            // the "scss" and "sass" values for the lang attribute to the right configs here.
            // other preprocessors should work out of the box, no loader config like this necessary.
            scss: 'vue-style-loader!css-loader!postcss-loader!sass-loader',
            sass: 'vue-style-loader!css-loader!postcss-loader!sass-loader?indentedSyntax',
            css: 'vue-style-loader!css-loader!postcss-loader'
          }
          // other vue-loader options go here
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10000,
            name: utils.assetsPath('img/[name].[hash:7].[ext]')
          }
        }
      }
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './public/index.html',
      inject: true
    }),
    new VueLoaderPlugin()
  ]
}
```