A look at how Phoenix configures Webpack per environment and how to expand upon that.

Apr 14, 2020

Phoenix is a great framework that gives you some out of the box configurations that are good enough for hitting the ground running, but that does not mean they are the best configurations to stick with long term.

One of the first things you may notice is that Phoenix generates a single webpack.config.js in the assets directory. What you may not know is that Webpack has optimizations that target different environments. Webpack uses a config option called Mode to set some of the other config options depending on the given mode of the webpack config. Read more about webpack’s mode option here https://webpack.js.org/configuration/mode/

As per the docs, there are two main ways you can supply the mode.

  • Via the CLI
  • Via the config option module.exports.mode option in the config.

If you opened up your dev config via config/dev.ex You will notices that Phoenix does provide the mode option under the dev env.

config :my_application, MyApplicationWeb.Endpoint,
 ...
watchers: [
  node: [
    "node_modules/webpack/bin/webpack.js",
    "--mode",
    "development",
    "--watch-stdin",
    cd: Path.expand("../assets", __DIR__)
  ]
]

And for production when deploying as pre https://hexdocs.pm/phoenix/deployment.html#compiling-your-application-assets

You will notice the call to npm run deploy –prefix ./assets which in turn calls into the npm’s assets/package.json scripts section which if we look calls.

…
"scripts": {
  "deploy": "webpack --mode production",
},

Now we can see Phoenix is calling webpack –mode development while running mix phx.server for local development and that calling npm run deploy –prefix ./assets as a part of a deployment process is really calling webpack –mode production

This is a reasonable setup but at some point, you really should consider that there are more options beyond what is being toggled via mode option. For example, say we wanted to manage how our source maps are being generated we would see that there is a whole section under DevTools that defaults to eval for development. If we wanted to configure that in an explicit way, we could set up conditions in our webpack config as shown in the mode docs for Webpack.

module.exports = (env, argv) => {
  if (argv.mode === 'development') {
    config.devtool = 'source-map';
  }
  if (argv.mode === 'production') {
    //...
  }
  return config;
};

At that point, I would advise you to just create separate webpack config files

So for example, I have two separate webpack config files in my assets directory.

  • webpack.dev.config.js
  • webpack.prod.config.js

And instead of calling –mode=env via the cli I set it vai the export mode option in the config its self.

IE:

module.exports = (env, options) => ({
  mode: "production"
}

Then for my local development I update my config/dev.ex watcher for my Endpoint.

watchers: [
  node: [
    "node_modules/webpack/bin/webpack.js",
    "--config",
    "webpack.dev.config.js",
    "--watch-stdin",
    cd: Path.expand("../assets", __DIR__)
  ]
]

You will notice that in place of

"--mode",
"development",

I have

"--config",
"webpack.dev.config.js",

Were I namespace my dev config as webpack.dev.config.js

Likewise, for deployment, I have updated the script’s deploy option on the package.json file.

"scripts": {
  "deploy": "webpack --config webpack.prod.config.js",
},

The big advantage to all of this is that I now have an explicit config for each Env respectively and I don’t have to use inline conditions to check which options I should enable or not given the env. In my opinion, this makes for a much easier to read webpack config.

With each config being responsible for their own env I now have an ability to explicitly set each’s own optimization.

For Production I have:

optimization: {
  minimizer: [
    new TerserPlugin({ cache: true, parallel: true, sourceMap: false }),
    new OptimizeCSSAssetsPlugin({})
  ],
  splitChunks: {
    chunks: "all"
  }
},

And for development I have.

optimization: {
  splitChunks: {
    chunks: "all"
  },
  usedExports: true
},

The only difference currently between the two is the use of minimizer and the usedExports. You will also notice there is some redundancy of splitChunks. This brings up another point. This is not the only way to manage your webpack files. You could, for example, have a webpack config load in a common config and then another specific config as it relates to the mode or env options passed into the CLI. Personally, maybe someday I clean up my code to be that dry but for now, this little bit of redundancy is fine for me. All this is to say there is really no wrong/right way to do all this but rather you have options and you should not just settle for the default.