« Previous Tutorial Next Tutorial »

We’re back with part 2 of our brief foray into optimization. I promise we’ll get back to building out our login system very shortly, but sometimes it’s better to optimize before writing a bunch more code, rather than after.

We’ve cut our Webpack bundle in half by using the uglify plugin on our production builds, but we’re still serving those builds from a built-in server, and we’ve still got hot-reloading enabled in production. Today we’re going to fix both of those issues.

Let’s start by opening webpack.config.js. Sick of this file, yet? I don’t blame you, but it’s one of the main parts of our app. Not quite the heart or brain, but at least the pancreas or liver. Anyway, we don’t actually have too many changes to make here, but we need to split out hot module reloading so that it’s only run when we run our dev server. Also, since we’re going to be creating static files for production, we need to adjust our output block. Let’s do that first. We’re going to start by creating several ternary operators that check our environment. A quick reminder on how ternaries work. They’re basically this:

const final = is a condition met? if yes return X : if no return Y;

So, for example, here’s a simple one:

const myNumber = 12
const numberDescription = myNumber > 10 ? 'bigger than ten!' : 'not bigger than ten!';

If you change the value of myNumber, you’ll get one of the two strings based on whether or not the value is, in fact, bigger than ten. That’s a useless ternary that would actually be better as a function that takes a number argument, but you get the idea. Let’s write one that’s actually useful. Add this on line 5:

const cssOutputLocation = process.env.NODE_ENV === 'production' ?
  'public/stylesheets/style-prod.css' :
  'stylesheets/style.css';

We’re splitting it into multiple lines because ESLint doesn’t like lines over 100 characters long, but it’s still the same approach. Are we in production? If yes, output a static file called “style-prod.css” to our public folder. If not, our output will be slightly different since it’ll be served by the Webpack Express middleware. The correct destination is stored in our cssOutputLocation variable, which we’ll use lower in the file.

Now we need to do the same for our JavaScript. This is a little more complex, because the output block isn’t a single line. Rather than try to stuff a bunch of objects into a ternary, we’re going to create our objects first. Just below the cssOutputLocation lines, add the following:

const jsProdOutput = {
  filename: 'public/javascripts/build-prod.js',
  path: resolve(__dirname),
  publicPath: '/',
};

const jsDevOutput = {
  filename: 'javascripts/build.js',
  path: '/',
  publicPath: '/',
};

Again, this will allow us to differentiate between production static files, and the dev files that our server will serve up. The different names means Express’s static file serving middleware won’t cause conflicts with the dev server, which is always handy (if we just named it “build.js” in both cases, it’d break things).

OK, now we can write our ternary like this:

const jsOutputLocation = process.env.NODE_ENV === 'production' ? jsProdOutput : jsDevOutput;

Excellent. Now we’ve got cssOutputLocation and jsOuputLocation variables. We’ll use those in a second. We could also just go with separate Webpack config files for production or development, and we probably will do exactly that in the long run, but I like doing it conditionally first so we can really get a sense of what the differences are while looking at a single file.

Now we need to modify our entry block. Right now it’s always running through a bunch of hot-loading entry points. That’s not useful for production. Skip down to the very bottom of the file, and add the following code:

if (process.env.NODE_ENV !== 'production') {
  module.exports.entry.unshift(
    'react-hot-loader/patch',
    'react-hot-loader/babel',
    'webpack-hot-middleware/client',
  );
}

Array.unshift() sticks stuff at the beginning of an array (as opposed to Array.push() which puts stuff at the end), so now we’re only adding our hot-reloading stuff when we’re not in production. Well, almost. Scroll back up, and delete those three lines from your entry block, so it looks like this:

  entry: [
    './index.jsx',
  ],

Now, when Webpack runs, if it’s not in production, it’ll toss those three lines back in ahead of index.js … but if we are in production, it won’t include them. This will generate more file size savings. All right, let’s replace our output block with our jsOutputLocation variable. Your entire output section should now look like this:

output: jsOutputLocation,

And let’s do the same for our CSS. Scroll down to the plugins section, and replace this line:

    new ExtractTextPlugin('stylesheets/style.css'),

with this one:

    new ExtractTextPlugin(cssOutputLocation),

There’s only one more thing to do in this file. Did you notice that we’re still calling the Hot Module Replacement Plugin regardless of whether we’re in prod or dev? Let’s fix that. Cut that line out of the plugins block, and in the if statement at the bottom of your file, add it below the entry lines, like this:

  module.exports.plugins.unshift(new webpack.HotModuleReplacementPlugin());

So the entire if statement will look like this:

if (process.env.NODE_ENV !== 'production') {
  module.exports.entry.unshift(
    'react-hot-loader/patch',
    'react-hot-loader/babel',
    'webpack-hot-middleware/client',
  );
  module.exports.plugins.push(new webpack.HotModuleReplacementPlugin());
}

That’s it for this file. For reference, here's the entire thing:

const { resolve } = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

const cssOutputLocation = process.env.NODE_ENV === 'production' ?
  'public/stylesheets/style-prod.css' :
  'stylesheets/style.css';

const jsProdOutput = {
  filename: 'public/javascripts/build-prod.js',
  path: resolve(dirname),
  publicPath: '/',
};

const jsDevOutput = {
  filename: 'javascripts/build.js',
  path: '/',
  publicPath: '/',
};

const jsOutputLocation = process.env.NODE_ENV === 'production' ? jsProdOutput : jsDevOutput;


module.exports = {
  context: resolve(dirname, 'src'),
  entry: [
    './index.jsx',
  ],
  output: jsOutputLocation,
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components|public\/)/,
        loader: 'babel-loader',
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: 'css-loader',
        }),
      },
      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
          use: [
            {
              loader: 'css-loader',
            },
            {
              loader: 'sass-loader',
            },
          ],
          fallback: 'style-loader',
        }),
      },
    ],
  },
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    new ExtractTextPlugin(cssOutputLocation),
  ],
};

if (process.env.NODE_ENV === 'production') {
  module.exports.plugins.push(new webpack.optimize.UglifyJsPlugin());
}

if (process.env.NODE_ENV !== 'production') {
  module.exports.entry.unshift(
    'react-hot-loader/patch',
    'react-hot-loader/babel',
    'webpack-hot-middleware/client',
  );
  module.exports.plugins.push(new webpack.HotModuleReplacementPlugin());
}

Save it, and then open up app.js. Find lines 47 through 59, which enable our webpack dev server, and wrap them in a check for a production environment, like this:

// Webpack Server
if (process.env.NODE_ENV !== 'production') {
  const webpackCompiler = webpack(webpackConfig);
  app.use(webpackDevMiddleware(webpackCompiler, {
    publicPath: webpackConfig.output.publicPath,
    stats: {
      colors: true,
      chunks: true,
      'errors-only': true,
    },
  }));
  app.use(webpackHotMiddleware(webpackCompiler, {
    log: console.log,
  }));
}

There we go. Now our app will only use the dev server when we’re not in production. Save this file, and move on to /views/index.ejs. We need to make some changes here to determine where to look for our CSS and JS files. Change line 5:

    <link rel='stylesheet' href='/stylesheets/style.css' />

to look like this:

    <% if (process.env.NODE_ENV === 'production') { %>
      <link rel="stylesheet" href="/stylesheets/style-prod.css" />
    <% } else { %>
      <link rel="stylesheet" href="/stylesheets/style.css" />
    <% } %>

And change what is now line 14:

  <script src="/javascripts/build.js"></script>

to look like this:

  <% if (process.env.NODE_ENV === 'production') { %>
      <script src="/javascripts/build-prod.js"></script>
  <% } else { %>
    <script src="/javascripts/build.js"></script>
  <% } %>

If your syntax highlighting is complaining about those blocks, I strongly recommend you install the “EJS 2” package for Sublime, then choose “Open all with current extension as” from the syntax menu on the bottom right, and find “EJS 2: EJS <% %>.” That should solve the problem.

Save this file, and open package.json. It’s time to create scripts to build for production, and run our server in production mode. Under line 6, add the following for Windows:

    "start-prod": "SET NODE_ENV=production&& node ./bin/www",

or this code for Mac/Unix:

    "start-prod": "NODE_ENV=production node ./bin/www",

Then delete line 10 because we don’t build to dev right now, we run a dev server, and replace it with this for Windows:

"build-prod": "SET NODE_ENV=production&&webpack -p --config webpack.config.js"

or this for Mac/Unix:

"build-prod": "NODE_ENV=production webpack -p --config webpack.config.js"

Save this file, and we’re done. Switch to a terminal window or command prompt, kill your server if it’s currently running, and type the following:

yarn run build-prod

Watch it go, and check out those numbers!

Asset    Size  Chunks                    Chunk Names
public/javascripts/build-prod.js  562 kB       0  [emitted]  [big]  main
public/stylesheets/style-prod.css  148 kB       0  [emitted]         main

We’re down to 562 KB of JavaScript from an initial 2.78 MB! That’s a savings of about 80%, which is a vast improvement with just these few little tweaks. And we’re still going to optimize some more in a later tutorial.

All right, let’s run this and make sure it works. Type:

yarn run start-prod

If you get an error here where Webpack is complaining about an unexpected parentheses, it’s because we’re using a trailing comma in the final lines of webpack.config.js. Webpack doesn’t recognize ES6 by default, so that trailing comma is confusing it. The fix is very simple! Just rename webpack.config.js to webpack.config.babel.js and because we already have babel available, Webpack will auto-transpile it. Woohoo! Just remember to update your scripts in package.json from "webpack.config.js to webpack.config.babel.js"

You’ll note that no Webpack build gets triggered here, which is what we want. Head to a browser and check your console. Yep … no hot module reloader, either. Perfect. We’ve gotten organized and are ready to proceed with wiring up our login page.

You may also notice that if you go back to running your development server, the world will explode and fire will rain from the skies. Well, or it'll fail. One of the two. The fix for this is detailed in tutorial 45, but if you want a head start, here's the condensed version:

  1. Run yarn add babel-register in your terminal
  2. Add require('babel-register'); as the first line of app.js
  3. Change line 17 of app.js to const webpackConfig = require('./webpack.config.babel');

You should be good to go, whether you're running a prod build or your dev server!

See you in the next tutorial!

« Previous Tutorial Next Tutorial »