React: Bake from Scratch or Box (JavaScript Version): Part 6

John Tucker
codeburst
Published in
7 min readDec 1, 2018

--

Continuing side-by-side comparison considering various optimizations.

This is part of a series starting with React: Bake from Scratch or Box (JavaScript Version): Part 1; a side-by-side comparison of creating React applications using a custom-built build solution (from scratch) versus using Create React App (from box).

Round 7: Tree Shaking

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export.

— webpack — Tree Shaking

One of the keys to tree shaking is being able to identify imports that have side-effects:

A “side effect” is defined as code that performs a special behavior when imported, other than exposing one or more exports. An example of this are polyfills, which affect the global scope and usually do not provide an export.

— webpack — Tree Shaking

Round 7: Tree Shaking — Scratch

note: The final version (of this round) of the custom-built build solution is available for download from the tree-shaking branch of the webpack-scratch-box repository.

Before we start to look at our code, there are some general issues that we need to address.

First we are going to remove the feature of @babel/preset-env that optimized our polyfills by removing the useBuiltins entry in .babelrc. Since polyfills are an integral part of dealing with tree shaking; this change simplifies how we handle them.

Also, spent a better part of an afternoon troubleshooting why tree shaking was not working; ended up tracking it down to an issue with react-hot-loader. Because tree shaking (production feature) is more important than hot loading (a development feature) for me, I am removing it from our custom-built build solution. Amounts to:

  1. Removing any reference to it from the application entry point, src / index.jsx
  2. Remove the plugin, react-hot-loader/babel, from .babelrc
  3. Removing react-hot-loader from package.json
  4. Removing the hot flag in the start script in package.json

Now, looking the code we have developed thus far, we have the two polyfills in the entry file that have side-effects:

import '@babel/polyfill';
...
import 'whatwg-fetch';
...

We also import global css that has side effects:

...
import './styles.css';
...

With this in mind, we need to identify which files have side effects in package.json:

{
...
"sideEffects": [
"@babel/polyfill",
"whatwg-fetch",
"*.css",
"*.scss",
"*.sass"
],
...
}

note: Because even CSS modules can define global CSS, we need to treat any CSS as potentially having side effects.

With this in place, our production builds will have dead code removed; modules with declared side effects are however included.

Round 7: Tree Shaking — Box

After all the fussing around with tree shaking and declaring modules having side effects, I was surprised to see that Create React App had not much to say about tree shaking (or side effects for that matter).

Did some basic testing and validated that Create React App was indeed tree shaking and removing unused exports from production bundles. At the same time, it did not appear to remove modules with side effects.

As a side-note, Create React App does not support hot reloading too; the work-arounds for this appear to be fairly fragile (would avoid).

Round 7: Tree Shaking — Comparison

Bottom line, it appears that tree shaking is fully supported in both our custom-built build and Create React App solutions. The fact that Create React App has no configuration nor documentation for the feature does make me a little nervous.

Round 8: Source Maps

The JavaScript sources executed by the browser are often transformed in some way from the original sources created by a developer…

In these situations, it’s much easier to debug the original source, rather than the source in the transformed state that the browser has downloaded. A source map is a file that maps from the transformed source to the original source, enabling the browser to reconstruct the original source and present the reconstructed original in the debugger.

Firefox Developer — Use a Source Map

At the same time, generating source maps is a time-consuming operation that should be avoided during development builds. The contradiction, however, is that it is during development that we are most likely want to use the debugger.

The solution is to use something that sufficiently resembles a true source map and yet is performant.

Round 8: Source Maps — Scratch

note: The final version (of this round) of the custom-built build solution is available for download from the source-maps branch of the webpack-scratch-box repository.

webpack provides a large number of options, which I personally find confusing; from what I can tell the cheap-module-eval-source-map is the strikes the balance of being fast on rebuilds and provides the original source.

...
const config = env => ({
...
devtool: env && env.NODE_ENV === 'production' ? 'source-map' : 'cheap-module-eval-source-map',
...
});
...

Round 8: Source Maps — Box

Create React App provides source maps in both development and production builds. While I cannot tell for sure, it appears that they also use cheap-module-eval-source-map under the hood.

Round 8: Source Maps — Comparison

Not much to say here, both solutions appear to operate the same way; they both provide true source maps on production builds and sufficiently good and performant ones on development builds.

Round 9: Minification

Minification (also minimisation or minimization), in computer programming languages and especially JavaScript, is the process of removing all unnecessary characters from source code without changing its functionality. These unnecessary characters usually include white space characters, new line characters, comments, and sometimes block delimiters, which are used to add readability to the code but are not required for it to execute.

— Wikipedia — Minification

We will want to minify both our JavaScript and CSS.

Round 9: Minification — Scratch

note: The final version (of this round) of the custom-built build solution is available for download from the minification branch of the webpack-scratch-box repository.

While webpack 5 is likely to come with a CSS minimizer built-in, with webpack 4 you need to bring your own. To minify the output, use a plugin like optimize-css-assets-webpack-plugin. Setting optimization.minimizer overrides the defaults provided by webpack, so make sure to also specify a JS minimizer:

— webpack — MiniCssExtractPlugin

We first install the dependencies:

npm install --save-dev uglifyjs-webpack-plugin
npm install --save-dev optimize-css-assets-webpack-plugin

We then update webpack.config.js:

...
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
...
const config = env => ({
...
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsPlugin({}),
],
},
});
...

Round 9: Minification — Box

Create React App minifies both the JavaScript and CSS.

Round 9: Minification — Comparison

Not much to say here other than they both support JavaScript and CSS minification.

Round 10: Caching

So we’re using webpack to bundle our modular application which yields a deployable /dist directory. Once the contents of /dist have been deployed to a server, clients (typically browsers) will hit that server to grab the site and its assets. The last step can be time consuming, which is why browsers use a technique called caching. This allows sites to load faster with less unnecessary network traffic, however it can also cause headaches when you need new code to be picked up.

This guide focuses on the configuration needed to ensure files produced by webpack compilation can remain cached unless their contents has changed.

webpack — Caching

The general solution is to have the server flag everything (but index.html) to cache indefinitely; index.html is flagged to never cache. The key, however, to “busting the cache” is to have all the assets (JavaScript, CSS, images, etc) be named to include a hash of their contents. So when the contents change, the filename changes, and the browser will download an updated version.

Round 10: Caching — Scratch

note: The final version (of this round) of the custom-built build solution is available for download from the caching branch of the webpack-scratch-box repository.

Now that we are building files with new filenames based on their contents, we need to clean the dist folder on every production build; otherwise it will be cluttered with unused files. The solution is clean-webpack-plugin:

npm install --save-dev clean-webpack-plugin

and update webpack.config.js

...
const CleanWebpackPlugin = require('clean-webpack-plugin')
...
const config = env => ({
...
plugins: [
...
new CleanWebpackPlugin(['dist']),
...
],
...
});
module.exports = config;

While images, fonts, etc. already are created with filenames based on their hash, we need to change webpack.config.js for the JavaScript and CSS files:

...
const config = env => ({
...
output: {
...
filename: env && env.NODE_ENV === 'production' ? '[name].[contenthash].js' : '[name].js',
},
...
plugins: [
...
env && env.NODE_ENV === 'production'
? new MiniCssExtractPlugin({
chunkFilename: '[id].css',
filename: '[name].[contenthash].css',
})
: new NothingPlugin(),
],
...
});
module.exports = config;

Round 10: Caching — Box

Create React App performs caching using the same pattern. It also, disabled by default, also supports caching using service workers.

Round 10: Caching — Comparison

Not much to say here, both solutions support the general approach to caching.

Create React App does have features to support caching with service workers; not having used this feature, I find having my code cluttered with it slightly annoying.

Wrap Up

In the next article, React: Bake from Scratch or Box (JavaScript Version): Part 7, we continue our side-by-side comparison by looking at module splitting among other topics.

--

--