Developing config is adding a lot of heavy processing in order to have source mapping working and debugging easy, but we don’t need this in production. All we need is speed. Let’s create another config that we are going to use for production build.
There are different ways to achieve this. One way for example is to create a common config and one for production and one for development. Then use webpack-merge plugin to combine the configs like it was explained here , or we could simply create another Webpack config for running the production server, but then we have to manage multiple configs which is far from perfect, so let’s use the same concept like before and extend (and override) the base config with production one.
Creating production config.
Add
./webpack.prod.config.js
const webpack = require('webpack');
let config = require('./webpack.base.config.js');
config.mode = "production";
config.devtool = "";
config.entry = [
'./src/index.js',
];
module.exports = config;
also lets modify the base config to use plain JavaScript syntax since Webpack won’t know how to run it and at this point there is no Babel teanspiler to convert the code to plain JavaScript.
./webpack.base.config.js
const getEnvironmentConstants = require('./getEnvironmentConstants');
const webpack =require('webpack');
module.exports = {
mode: 'development',
devtool: 'eval-source-map',
entry: [
'@babel/polyfill',
'./src/index.js',
],
output: {
filename: '[name]-bundle.js',
publicPath: '/dist/',
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
// SCSS
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 2,
localIdentName: '[folder]-[local]',
sourceMap: true
}
},
{
loader: 'postcss-loader',
options: {
plugins: () => [require('autoprefixer')()],
sourceMap: true
},
},
{
loader: 'sass-loader',
options: {
outputStyle: 'expanded',
sourceMap: true
}
}
],
},
// images
{
test: /\.(png|jp(e*)g|svg)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8000, // Convert images < 8kb to base64 strings
name: 'images/[hash]-[name].[ext]'
}
}]
},
//File loader used to load fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader']
}
]
},
plugins: [
new webpack.DefinePlugin({ 'process.env' : getEnvironmentConstants() } )
]
};
Creating production build.
Edit package.json and add clean script to remove the dist
folder and build-prod
script.
./package.json
...
"scripts": {
"start-cli": "webpack-dev-server --hot --history-api-fallback --config webpack.cli.config.js",
"start-api": "babel-node server-api.js",
"start-middleware": "babel-node server-middleware.js",
"clean": "rm -rf ./dist",
"lint": "eslint .",
"build-dev": "webpack --mode development",
"build-prod": "webpack --config webpack.prod.config.js",
},
...
what we just did:
– we added a cleanup script clean
that will clean up the production build directory (line 6)
– we added a production build script (line 9)
Now we could clear the ./dest
folder by running yarn clean
then do the production build yarn build-prod
and have production bundle ready.
Creating Express production server.
There are different ways to serve your files once the production build is done. One way is to use nginx server which I might create a tutorial of how to set up later, or we could use the express server.
Let’s create an Express config and run a simple prod server. Create the prod.server.js
with the following contents:
./prod.server.js
const express = require('express');
const path = require('path')
const app = express();
const port = 8080;
app.use('/dist', express.static('dist'));
app.get('*', (req, res)=>{
res.sendFile(path.join(__dirname, 'index.html'));
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
what we just did:
– we created an Express server
– we told the server to serve every static file like JS, CSS, images from the /dist
folder (line 6)
– everything else that is not matching the static content falls into ‘*’ and will serve index.html
Looks familiar? It’s pretty similar to that we did in the previous chapters.
Let’s add a production server start script:
./package.json
"scripts": {
"start-cli": "webpack-dev-server --hot --history-api-fallback --config webpack.cli.config.js",
"start-api": "babel-node server-api.js",
"start-middleware": "babel-node server-middleware.js",
"clean": "rm -rf ./dist",
"lint": "eslint .",
"build-dev": "webpack --mode development",
"build-prod": "webpack --config webpack.prod.config.js",
"run-prod-server": "node ./prod.server.js",
"build-and-run-prod-server": "yarn clean && yarn build-prod && yarn run-prod-server"
},
what we just did:
– we created `run-prod-server` that will simply start the express server with will statically serve the contents of the /dist
folder where build-prod
script is dumping the bundle.
– To make things easier we also created a new script entry (line 10) that does all necessary steps to run production server:
– calls yarn clean
script to remove the ./dist
folder before rebuilding
– calls yarn build-prod
to make a fresh production buid
– tells node to start using ./prod.server.js
that we just created as an entry point, which starts the Express server.
Give it a try yarn run-prod-server
and observe the ./dist
folder. It will disappear for a brief moment, then it will re-appear with the fresh content and once you see this message in the console ‘example app listening on port 8080!’ you are ready to hit the browser url.
If everything went well, you should see the site running, using the production Express server.
Extracting CSS in a separate files.
Right now, if we look at the ./dist
folder we will see only *.js and the font file that we added, which means that all CSS is embedded into Javascript files, which is ok for development, but far from ideal for production. Now the browser can’t cache the CSS, and we will see issues with styling the components since it will take time for the CSS to be applied. To solve this problem we have to extract the CSS from JS and load it separately.
Adding mini-css-extract-plugin.
And there is a plug-in for this called mini-css-extract-plugin. Let’s install it.
yarn add mini-css-extract-plugin
and let’s tell Webpack to use it only with the production build:
./webpack.prod.config.js
const webpack = require('webpack');
let config = require('./webpack.base.config.js');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
config.mode = "production";
config.devtool = "";
config.entry = [
'./src/index.js',
];
config.module.rules[1].use[0] = MiniCssExtractPlugin.loader;
config.plugins = [ ... [new MiniCssExtractPlugin({
// these are optional
filename: "[name].css",
chunkFilename: "[id].css"
})],
... config.plugins
];
module.exports = config;
what we just did:
– we replaced ‘style-loader’ with mini-css-extract-plugin
to extract all CSS files form JS bundle (line 12)
– by default even without any parameters mini-css-extract-plugin will create two bundle types: one with the main.css and the rest per component base. Which means that each component will have their own CSS bundle, dynamically loaded when the component is rendered. For a large project this will dramatically reduce the initial CSS load.
You could play with the optional parameters: filename
and chunkFilename
and see how this will affect generated CSS files names. If for example you would like to rename the main file to global-main.css
you could set filename: "global-[name].css"
or if you want to change the chunk names you could do this: chunkFilename: "[id].css"
. Parameters in the brackets are automatically replaced by Miini-css-extract-plugin. [id] – will be replaced with the chunk id # for example.
Rebuild the project:
yarn build-prod
And let’s look at ./dist
folder. Below every JS bundle we should be able to see the corresponding CSS for this module. Now it looks better. Run the production server build-and-run-prod-server
load the project and observe the net tab while navigating in different site sections. You will see the css files loading for each component once you navigate to a new section. But we are missing the main.css which is not loading on demans. Let’s add a tag to load it:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Babel Webpack Boilerplate</title>
<link rel="stylesheet" type="text/css" href="dist/main.css">
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="dist/main-bundle.js"></script>
</body>
</html>
Run the production server again and the app should look like before.
But there is another problem: click to see the file content’s and you will notice that the CSS is not minified.
Minimizing the CSS.
Let’s use optimize-css-assets-webpack-plugin
yarn add optimize-css-assets-webpack-plugin --dev
./webpack.prod.config.js
const webpack = require('webpack');
let config = require('./webpack.base.config.js');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
config.mode = "production";
config.devtool = "";
config.entry = [
'./src/index.js',
];
config.module.rules[1].use[0] = MiniCssExtractPlugin.loader;
config.plugins = [ ... [new MiniCssExtractPlugin({
// these are optional
filename: "[name].css",
chunkFilename: "[id].css"
})],
new OptimizeCSSAssetsPlugin({}),
... config.plugins
];
module.exports = config;
Rebuild production and run the prod server and if you open some of the CSS file contents you will see that they are now minified.
Just what we need for production build!