Monday, 26 March 2018

It's Not Dead 2: mobx-react-devtools and the undead

I spent today digging through our webpack 4 config trying to work out why a production bundle contained code like this:

if("production"!==e.env.NODE_ENV){//...

My expectation was that with webpack 4 and 'mode': 'production' this meant that behind the scenes all process.env.NODE_ENV statements should be converted to 'production'. Subsequently Uglify would automatically get its groove on with the resulting if("production"!=="production") ... and et voilĂ !... Strip the dead code.

It seemed that was not the case. I was seeing (regrettably) undead code. And who here actually likes the undead?

Who Betrayed Me?

My beef was with webpack. It done did me wrong. Or... So I thought. webpack did nothing wrong. It is pure and good and unjustly complained about. It was my other love: mobx. Or to be more specific: mobx-react-devtools.

It turns out that the way you use mobx-react-devtools reliably makes the difference. It's the cause of the stray ("production"!==e.env.NODE_ENV) statements in our bundle output. After a long time I happened upon this issue which contained a gem by one Giles Butler. His suggested way to reference mobx-react-devtools is (as far as I can tell) the solution!

On a dummy project I had the mobx-react-devtools advised code in place:

import * as React from 'react';
import { Layout } from './components/layout';
import DevTools from 'mobx-react-devtools';

export const App: React.SFC<{}> = _props => (
    <div className="ui container">
        <Layout />
        {process.env.NODE_ENV !== 'production' ? <DevTools position={{ bottom: 20, right: 20 }} /> : null}
    </div>
);

With this I had a build size of 311kb. Closer examination of my bundle revealed that my bundle.js was riddled with ("production"!==e.env.NODE_ENV) statements. Sucks, right?

Then I tried this instead:

import * as React from 'react';
import { Layout } from './components/layout';
const { Fragment } = React;

const DevTools = process.env.NODE_ENV !== 'production' ? require('mobx-react-devtools').default : Fragment;

export const App: React.SFC<{}> = _props => (
    <div className="ui container">
        <Layout />
        <DevTools position={{ bottom: 20, right: 20 }} />
    </div>
);

With this approach I got a build size of 191kb. This was thanks to the dead code being actually stripped. That's a saving of 120kb!

Perhaps We Change the Advice?

There's a suggestion that the README should be changed to reflect this advice - until that happens, I wanted to share this solution. Also, I've a nagging feeling that I've missed something pertinent here; if someone knows something that I should... Tell me please!

Sunday, 25 March 2018

Uploading Images to Cloudinary with the Fetch API

I was recently checking out a very good post which explained how to upload images using React Dropzone and SuperAgent to Cloudinary.

It's a brilliant post; you should totally read it. Even if you hate images, uploads and JavaScript. However, there was one thing in there that I didn't want; SuperAgent. It's lovely but I'm a Fetch guy. That's just how I roll. The question is, how do I do the below using Fetch?

  handleImageUpload(file) {
    let upload = request.post(CLOUDINARY_UPLOAD_URL)
                     .field('upload_preset', CLOUDINARY_UPLOAD_PRESET)
                     .field('file', file);

    upload.end((err, response) => {
      if (err) {
        console.error(err);
      }

      if (response.body.secure_url !== '') {
        this.setState({
          uploadedFileCloudinaryUrl: response.body.secure_url
        });
      }
    });
  }

Well it actually took me longer to work out than I'd like to admit. But now I have, let me save you the bother. To do the above using Fetch you just need this:

  handleImageUpload(file) {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("upload_preset", CLOUDINARY_UPLOAD_PRESET); // Replace the preset name with your own

    fetch(CLOUDINARY_UPLOAD_URL, {
      method: 'POST',
      body: formData
    })
      .then(response => response.json())
      .then(data => {
        if (data.secure_url !== '') {
          this.setState({
            uploadedFileCloudinaryUrl: data.secure_url
          });
        }
      })
      .catch(err => console.error(err))
  }

To get a pre-canned project to try this with take a look at Damon's repo.

Wednesday, 7 March 2018

It's Not Dead: webpack and dead code elimination limitations

Every now and then you can be surprised. Your assumptions turn out to be wrong.

Webpack has long supported the notion of dead code elimination. webpack facilitates this through use of the DefinePlugin. The compile time value of process.env.NODE_ENV is set either to 'production' or something else. If it's set to 'production' then some dead code hackery can happen. Libraries like React make use of this to serve up different, and crucially smaller, production builds.

A (pre-webpack 4) production config file will typically contain this code:

new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify('production')
}),
new UglifyJSPlugin(),

The result of the above config is that webpack will inject the value 'production' everywhere in the codebase where a process.env.NODE_ENV can be found. (In fact, as of webpack 4 setting this magic value is out-of-the-box behaviour for Production mode; yay the #0CJS!)

What this means is, if you've written:

if (process.env.NODE_ENV !== 'production') {
  // Do a development mode only thing
}

webpack can and will turn this into

if ('production' !== 'production') {
  // Do a development mode only thing
}

The UglifyJSPlugin is there to minify the JavaScript in your bundles. As an added benefit, this plugin is smart enough to know that 'production' !== 'production' is always false. And because it's smart, it chops the code. Dead code elimated.

You can read more about this in the webpack docs.

Limitations

Given what I've said, consider the following code:

export class Config {
    // Other properties

    get isDevelopment() {
        return process.env.NODE_ENV !== 'production';
    }
}

This is a config class that exposes the expression process.env.NODE_ENV !== 'production' with the friendly name isDevelopment. You'd think that dead code elimination would be your friend here. It's not.

My personal expection was that dead code elimination would treat Config.isDevelopment and the expression process.env.NODE_ENV !== 'production' identically. Because they're identical.

However, this turns out not to be the case. Dead code elimination works just as you would hope when using the expression process.env.NODE_ENV !== 'production' directly in code. However webpack only performs dead code elimination for the direct usage of the process.env.NODE_ENV !== 'production' expression. I'll say that again: if you want dead code elimination then use the injected values; not an encapsulated version of them. It turns out you cannot rely on webpack flowing values through and performing dead code elimination on that basis.

The TL;DR: if you want to elimate dead code then *always* use process.env.NODE_ENV !== 'production'; don't abstract it. It doesn't work.

UglifyJS is smart. But not that smart.