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

John Tucker
5 min readDec 2, 2018

Continuing side-by-side comparison considering code splitting among other topics.

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 10: React Hot Loader — Revisited

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

In the last article, we had to disable React Hot Loader to enable tree shaking. We also learned that Create React App does not support React Hot Loader (without some fragile work-arounds). Turns out that we can conditionally load React Hot Loader in development mode (key is there is a JavaScript version of the Babel configuration) with our custom-built build solution.

We install the dependency:

npm install react-hot-loader

We update the start script with the hot option in package.json. We also setup an environment variable, NODE_ENV, for the build (used later).

{
...
"scripts": {
...
"build-prod": "NODE_ENV=production webpack -p --mode production --env.NODE_ENV=production",
"start": "webpack-dev-server --open --hot --mode development",
"analyze": "NODE_ENV=production webpack -p --mode production --env.analyze --env.NODE_ENV=production"
},
...
}

note: Feels odd to have duplicate flags for production mode; left it in as different tools have different mechanisms for this.

In order to conditionally use the react-hot-loader/babel plugin, we switch to the JavaScript form of the Babel configuration.

Finally, we update the application entry point to conditionally include react-hot-loader:

...
import { hot } from 'react-hot-loader';
...
const AppWithHot = hot(module)(App);
const mountNode = document.getElementById('app');
const AppUsed = process.NODE_ENV === 'production' ? <App /> : <AppWithHot />;
render(AppUsed, mountNode);
...

With these changes, we have re-installed React Hot Loader without breaking tree shaking.

Round 11: Code Splitting

Code splitting is one of the most compelling features of webpack. This feature allows you to split your code into various bundles which can then be loaded on demand or in parallel. It can be used to achieve smaller bundles and control resource load prioritization which, if used correctly, can have a major impact on load time.

webpack — Code Splitting

One common (and desirable) use of code splitting is to pull all the third-party JavaScript files in the initial chunks (loaded from index.html) into a separate bundle (also loaded from index.html); often referred to as the vendor bundle. Why is this helpful?

First, it is common that while the application’s code changes frequently, its third-party libraries often do not. Thus as the application changes, browsers do not need to re-download the vendor bundle.

Another beneficial use of code splitting is using dynamic imports to segment the application so that only a minimal download is needed when the application first starts; additional code (including third-party) is downloaded on demand only when needed.

Round 11: Code Splitting — Scratch

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

Much of the work we have done thus far, production mode, html-webpack-plugin, content-hash, etc, have set us up for bundle splitting; including dynamic imports. We only need to add another optimization entry (splitChunks) to webpack-config.js.

The vendor sub-entry creates the vendor bundle.

The styles sub-entry extracts all of the CSS across all the bundles into a single CSS file that is loaded from index.html. The primary advantage of this is to avoid the Flash-of-Unstyled-Content (FOUC).

note: Based on trial and error, this might not actually be needed as it appears that the JavaScript waits for the CSS to load.

...
const config = env => ({
...
optimization: {
...
splitChunks: {
cacheGroups: {
vendor: {
test: /node_modules/,
chunks: 'initial',
name: 'vendor',
enforce: true,
},
styles: {
name: 'styles',
test: /\.(css|scss|sass)$/,
chunks: 'all',
enforce: true,
},
},
},
},
});
...

With this change, we are ready to use code splitting.

Round 11: Code Splitting — Box

Create React App mirrors the custom-built build solution (above) except it does not extract all the CSS into a single file (it creates a CSS file per bundle). At same time, the CSS from the entry bundles is included in the index.html file.

note: I originally thought this was going to be an issue; the components loading from the non-entry bundles being at risk of having FOUC. More recently, I confirmed by trial and error that it does not appear to be a problem (the JavaScript waits for the CSS to load before executing).

Round 11: Code Splitting — Comparison

Our custom-built build solution fully supports the ideal code splitting solution. Create React App gets close but has an issue (risk of FOUC) with the CSS used in the non-entry bundles.

Round 12: Environment

Just when I thought we were done, I recalled an important feature (more of a dev ops thing).

Your project can consume variables declared in your environment as if they were declared locally in your JS files. By default you will have NODE_ENV defined for you, and any other environment variables starting with REACT_APP_.

The environment variables are embedded during the build time. Since Create React App produces a static HTML/CSS/JS bundle, it can’t possibly read them at runtime.

Create React App — Adding Custom Environment

While environment variables are convenient for production builds through continuous integration, e.g., Travis CI, defining variables in an environment file (.env) is better for development builds.

Round 12: Environment — Scratch

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

The specific solution we will create for our custom-built build solution is:

  • From earlier work, the application variable process.env.NODE_ENV is appropriately embedded (swapped) to development or production
  • Any application variable starting with process.env.REACT_APP_ is swapped out for the matching entry in an .env file
  • If no matching entry in an .env file, the variable is swapped out for the matching environment variable starting with REACT_APP_
  • If neither matching entry in an .env file or matching environment variable, the variable is swapped out for undefined

The recommended approach is to use the .env file for development builds and not check the file into source control (git). Set environment variables in the continuous integration solution, e.t., Travis CI, for production builds.

We start by installing the dependencies:

npm install --save-dev dotenv

and update webpack.config.js

...
const dotenv = require('dotenv');
...
const ENV_REGEX = /^REACT_APP_/;
dotenv.config();
const envKeys = Object.keys(process.env).filter(key => key.match(ENV_REGEX));
const config = env => ({
...
plugins: [
...
new webpack.EnvironmentPlugin(envKeys),
],
...
});
module.exports = config;

Round 12: Environment — Box

Create React App has a similar, albeit a bit more complicated (in a good way) implementation.

Round 12: Environment — Compared

Both solutions have the core features of using both environment variables and .env files to swap variables during the build. The Create React App approach is more complicated (in a good way). Because we built the custom-built build solution, we could extend it as necessary.

Wrap Up

In the next (and last) article, React: Bake from Scratch or Box (JavaScript Version): Smackdown, we wrap up the series with a summary of our findings.

--

--

John Tucker

Broad infrastructure, development, and soft-skill background