dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

webpackでReact/Reduxを本番環境用にビルド(production build)する

はじめに

React に限らず、JavaScript を使ってると本番環境用のビルド(以後、production build)として圧縮や難読化を行う必要があります。
webpack にはそのためのプラグインとして UglifyJsPlugin があるのは知っていましたが、
単純にこのプラグインを適用しただけだとブラウザで以下のような warnings が出ていることがわかりました。

f:id:dackdive:20170307045306p:plain

  • Warning: It looks like you're using a minified copy of the development build of React. When deploying React apps to production, make sure to use the production build which skips development warnings and is faster. See https://fb.me/react-minification for more details.
  • You are currently using minified code outside of NODE_ENV === 'production'. This means that you are running a slower development build of Redux. You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) to ensure you have the correct code for your production build.

そのため、このエラーが表示されなくなるような、正しい production build 用の webpack の構成を一度調べてみたいと思います。

加えて、UglifyJsPlugin を使っていても redux-logger のログが出力されており、これは本番環境では出力されないことが望ましいので
そのような切り替えを行う方法についても一緒に調べてみます。

※ webpack v1を対象としていますが、分かる範囲で v2 との違いについても言及しています。


サンプル(GitHub

先に、今回の調査を踏まえたサンプルリポジトリはこちらです。

(この記事を書いたときの最新コミットはbdff7ec


やること1:webpack の各種プラグインを導入&設定する

使用する webpack のプラグインは以下です。


DefinePlugin で process.env.NODE_ENV を置換する

DefinePlugin はビルド実行時、コード中に埋め込まれたグローバル変数プラグインで定義した値に置換してくれるプラグインです。
たとえば

// webpack.config.js
plugins: [
  new webpack.DefinePlugin({
    PRODUCTION: JSON.stringify(true),
  }),
],

という定義があった場合、コード中の

if (PRODUCTION) {
  console.log('production');
}

if (true) {
  console.log('production');
}

に置き換えられます。

React などのライブラリには

if (process.env.NODE_ENV === 'development') {
  // only in development
}

のような処理が埋め込まれており、DefinePlugin でこの部分を置換します。

// webpack.config.js
plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    },
  }),
],

これの何が良いかと言うと、次に紹介する UglifyJsPlugin と組み合わせることで到達しないコードが削除され、ファイルサイズの圧縮につながります。
※到達しないコード = if (false) { ... } のような部分のこと


UglifyJsPlugin を production build 時のみ適用する

冒頭でも紹介した UglifyJsPlugin も webpack.config.js に追加します。
これは開発時には OFF になってほしいので、たとえば NODE_ENV=production を指定した時のみ有効になるよう、以下のようにします。

// webpack.config.js
const PRODUCTION = process.env.NODE_ENV === 'production';

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    },
  }),
  ...(
    PRODUCTION ? [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
        },
      }),
    ] : []
  ),
]    

UglifyJsPlugin のオプションですが、{ compress: { warning: false } } を指定しないとビルド時に

WARNING in bundle.js from UglifyJs
Condition always false [../~/react/lib/React.js:32,0]
Dropping unreachable code [../~/react/lib/React.js:32,0]
...

のような warning がずらっと出力されるので、指定しておきます。

また、参考にした create-react-app の webpack.config.prod.js ではそれ以外のオプションも色々と指定しているようですが、README に記載がないのとほとんどがデフォルト値のようなのでとりあえずこれでいいかと。


DedupePlugin, OccurenceOrderPlugin も production build 時のみ適用する

最後に、DedupePlugin と OccurrenceOrderPlugin という 2 つのプラグインも導入します。
DedupePlugin は重複するモジュールはひとつにまとめてくれるプラグイン、OccurenceOrderPlugin は利用頻度の高いモジュールのにをなるべく短い ID を降ることでファイルサイズの圧縮を図るプラグインという理解です。
先程の UglifyJsPlugin に続けてプラグインを定義します。

// webpack.config.js
const PRODUCTION = process.env.NODE_ENV === 'production';

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    },
  }),
  ...(
    PRODUCTION ? [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
        },
      }),
      new webpack.optimize.OccurrenceOrderPlugin(),
      new webpack.optimize.DedupePlugin(),
    ] : []
  ),
]    

DedupePlugin については 公式ドキュメント

Note: Don’t use it in watch mode. Only for production builds.

という記載があるので production build 時のみ適用すべきですが、OccurrenceOrderPlugin については特にそういう記述はないので常に有効でもいいのかもしれません。


補足:webpack v2 との差分

webpack v1 -> v2 の migration guide を読むと、今回紹介したプラグインで関係している変更は以下のあたりでしょうか。

  • UglifyJsPlugin
    • { compress: { warnings: false } } はデフォルトになったので指定不要に(参考
    • sourceMap がデフォルトで出力されなくなったので、出力したい場合は明示的に { sourceMap: true } を追加(参考
  • OccurrenceOrderPlugin
    • 指定しなくても有効になったので、記載不要(参考
  • DedupePlugin
    • こちらも webpack2 では不要(参考


やること2:NODE_ENV = production のときは redux-logger が読み込まれないようにする

webpack の設定が終わったので、続いて Redux まわりの設定を。
redux-logger が production build 時には読み込まれないようにします。

これは README にも記載がありますが、webpack のときと同様に process.env.NODE_ENV の値を使って
process.env.NODE_ENV === 'development' のときだけ redux-logger がインポートされるようにします。

reducer の Hot Module Replacement の設定も考慮すると、store の設定は以下のようになります。

// store/configureStore.js

/* eslint-disable global-require */
import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers';

// eslint-disable-next-line no-underscore-dangle
const __PRODUCTION__ = process.env.NODE_ENV === 'production';

export default function configureStore(initialState) {
  const middlewares = [];
  if (!__PRODUCTION__) {
    const createLogger = require('redux-logger');
    middlewares.push(createLogger());
  }
  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(...middlewares),
  );

  if (!__PRODUCTION__ && module.hot) {
    module.hot.accept('../reducers', () => {
      const newRootReducer = require('../reducers').default;
      store.replaceReducer(newRootReducer);
    });
  }

  return store;
}

これで production build 時には redux-logger がインポートされなくなりました。


おまけと TODO

冒頭で述べたブラウザの warning について

それぞれ ReactDOM および Redux 内で定義されていました。

NODE_ENV = production じゃない状態で minify されているかどうか」の判定方法、面白いですね。
(ダミーの function を定義し、function.name が関数名そのものと一致するかどうか判定し、一致しない = minify されている)

webpack の -p オプション

production build は webpack のデフォルトのオプションとしてもいくらかサポートされてるようです。
参考:https://webpack.github.io/docs/cli.html#production-shortcut-p

$ webpack -p

$ webpack --optimize-minimize --optimize-occurrence-order

というオプションをつけるのと同じで、それぞれのオプションについては https://github.com/webpack/docs/wiki/optimization を参照。

TODO

production build 時には console.log も除去したい。こちらを使えばできる?


リファレンス

webpack.config.js について

create-react-app の webpack.config.prod.js が参考になるほか、日本語記事では

Redux の configureStore.js について

この記事が参考になりました。
Practical Redux, Part 3: Project Planning and Setup · Mark's Dev Blog

ただ、記事では Redux Devtools などの設定も行っていますが、今回はそこまで手が出せなかった...