Webpack 3 + React — Production build tips

David Lazic
Jul 6, 2017 · 6 min read
Image for post
Image for post

Couple of days ago I had the honor to upgrade our project’s Webpack build as we strived to polish the app’s performance. Squeezing the maximum out of Webpack’s build definitely gave us significant improvements.

Here I will try to share some guidelines that could help you understand your Webpack’s configuration better and figure out a way to get the optimal setup for the project you’re working on.

Notice:

I’ve only shown parts of the code in examples below, but be wary that the plugins’ configuration order can be crucial for some of them.

Full gist is included at the bottom of this article.

If you’re still deciding whether to upgrade your Webpack 1, I’d say — definitely go for the upgrade. v1 → v2 could be a bit of a hassle, but v2 → v3 is in 98% of the cases smooth (per Webpack’s team statistics).

Migration alone will boost bundles’ performance and reduce their size significantly. We’ll obtain tree shaking feature in v2 and scope hoisting in v3. First one comes as a built-in feature starting from v2, but scope hoisting needs to be enabled in v3 by using ModuleConcatenationPlugin.

plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]

Instead of ending up with one huge bundle.js, we can split our app and vendor code into separate files.

We can do that by defining an entry point for our app code and then using a CommonsChunkPlugin to bundle everything from node_modules into vendor.

We’ll also add cache busting via chunkhash so that all of the output files have different hashes. Each files’ hash will stay the same with each build unless its file contents have changed.

entry: {
app: path.resolve(sourcePath, 'index.js')
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[chunkhash].js',
publicPath: '/'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.[chunkhash].js',
minChunks (module) {
return module.context &&
module.context.indexOf('node_modules') >= 0;
}
})
]

This type of optimization is still a debatable one. There are many factors to consider and a couple of different ways to achieve this, but eventually it really comes down to project’s characteristics.

Webpack 2 comes with an improvement regarding dynamic imports. While the legacy way was using require.ensure, latest release follows the TC39 standards proposal and uses the import() syntax.

On the other hand, react-router 4 came with improvements to lazy-load your components by using bundle-loader.

I haven’t had the time to test this in depth, but the things I’ve tried didn’t have much of an impact. You could even say that they degraded the performance on the project I’ve been working on.

Minifying Webpack’s output is really important in order to reduce its size.

html-webpack-plugin is a great tool for compiling .html files. We can use one of the templating engines (.hbs|.ejs) for the index file, which will be compiled to index.html.

const HtmlWebpackPlugin = require('html-webpack-plugin');plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'index.ejs'),
path: buildPath,
excludeChunks: ['base'],
filename: 'index.html',
minify: {
collapseWhitespace: true,
collapseInlineTagWhitespace: true,
removeComments: true,
removeRedundantAttributes: true
}
})
]

Minification of the .js output starts with built-in UglifyJsPlugin. Uglify’s compression config below is suggested by many tutorials online as one of the best for production builds.

There’s also an option to use identifiers instead of module names to minize the output a bit more. To enable this simply invoke HashedModuleIdsPlugin (NamedModulesPlugin is recommended for development).

plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true
},
output: {
comments: false
}
}),
new webpack.HashedModuleIdsPlugin()
]

We can further reduce the size of React’s vendor by using production NODE_ENV setting, which would remove all the development oriented warnings and overhead.

plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]

When a user’s connection is slow, we want the first paint to be as fast as possible, so the general idea is to identify critical, visual parts of the project and internalize their CSS directly on the page (e.g. general layout, header & footer, fonts). That way we’ll be able to provide perceptually faster load time.

We can simply split SCSS modules into base.scss and style.scss, where base would contain all those critical parts.

First step is to extract and minify all the CSS from webpack’s entry points as separate files. After that we run through the chunks and internalize critical ones inside the head section of the compiled index.html.

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const StyleExtHtmlWebpackPlugin = require('style-ext-html-webpack-plugin');
plugins: [
new ExtractTextPlugin({
filename: '[name].[contenthash].css',
allChunks: true
}),
new StyleExtHtmlWebpackPlugin({
minify: true
})
]

By choosing source map style we can further reduce the size of the build. Webpack’s devtools option enables you to set the appropriate style.

There are multiple style options, but generally most of the online guides suggest using cheap-module-source-map option for development, and source-map in production.

Note: Corrected devtools mode, thanks David Patrick!

devtool: 'source-map'

Setting one of these modes onto script tags will modify their loading order and can consequently lead to faster page load time as you’ll avoid render-blocking requests. Identify and separate the code that needs to be executed immediately from the code that could be executed later on.

With script-ext-html-webpack-plugin we can set different script options for compiling.

const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');plugins: [
new ScriptExtHtmlWebpackPlugin({
defaultAttribute: 'defer'
})
]

Warning: Async and defer attributes are essentially different in their execution time. Both types will load in parallel with HTML parsing. The difference is that async scripts will execute immediately after their load is complete, while deferred scripts will wait for the HTML parsing to be complete and will be executed in their respective order.

Consider using dns-prefetch links for certain domains if you need to use 3rd party code. This will ensure that the browser does DNS lookup before the asset is requested later on.

<link rel="dns-prefetch" href="https://www.<example_domain>.com">

Preloading will initiate early, high-priority, and non-render-blocking fetch of a resource which will be used later on. To add these at compile time, we can use preload-webpack-plugin.

const PreloadWebpackPlugin = require('preload-webpack-plugin');plugins: [
new PreloadWebpackPlugin({
rel: 'preload',
as: 'script',
include: 'all',
fileBlacklist: [/\.(css|map)$/, /base?.+/]
})
]

Gzipping build files will reduce their size by a huge amount. In order for this to work we’ll also need to do modifications on your webserver config (e.g. Apache|Nginx). If the browser supports gzip that’s what he’ll receive.

const CompressionPlugin = require('compression-webpack-plugin');plugins: [
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$|\.eot?.+$|\.ttf?.+$|\.woff?.+$|\.svg?.+$/,
threshold: 10240,
minRatio: 0.8
})
]

Consider using a service worker to cache your build files (all or some), if needed. There are some really great plugins that do this out of the box, such as offline-plugin or sw-precache-webpack-plugin, or you can write your own service worker.

This is how the end configuration file looked like.

I’ll add screenshots of the app’s load times in development and production environment with good 3G network throttling.

Notice that there’s some overhead included because of the loaded fonts and images, but that optimization topic is too wide for this article.

First case is development build. As shown below, app.js is enormous in size as we’ve bundled up everything with no optimizations.

app.83852f151d1eccbe4a08.js         2.63 MB
app.83852f151d1eccbe4a08.js.map 3.08 MB
index.html 1.31 kB
Image for post
Image for post
Environment: DEVELOPMENT

When we apply build optimizations we end up with split app and vendor files, main css file and base css which was injected directly inside index.html.

We reduced the size alone more than 89% and managed to improve load time more than 74%!

app.6c682e3a87517dd36425.js.gz                  21.9 kB
app.a5a5d15f6b07d45545a0794a327ab904.css.gz 30.9 kB
vendor.68f62e37ce5bcaf5df30.js.gz 220 kB
index.html 13.7 kB
Image for post
Image for post
Environment: PRODUCTION

It’s definitely worth upgrading and optimizing your Webpack configuration, especially if you want to build performant apps. You’ll see an improvement from version upgrade alone (we gained around 4 to 5 sec faster load time, v1 — > v3).

Lastly, I hope this article will help you achieve a bit more understanding of what are the general guidelines of improving your build and app’s performance with Webpack.

Netscape

A community dedicated to those who use JavaScript every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store