Day 2, Segment 4, Part 3, Code splitting

In notebook:
FrontEndMasters React Intro 2
Created at:
2016-12-29
Updated:
2016-12-30
Tags:
JavaScript Webpack libraries React
Branch v2-24

53:30

Code splitting

The last (dev) build of the app is 1.4MB. This is way too big. 
Even if you only visit the front page, you get all the code for all the other pages as well. 
Would be better if these could be loaded progressively. 

This is actually quite easy in Webpack. You have to signal to Webpack where you want your code to be cut off. 

57:00

We have to tell Webpack, where to download the additional bundles from. (Since we're splitting the code).
  // ****   webpack.config.js   ****

const path = require('path')

module.exports = {
  context: __dirname,
  entry: './js/ClientApp.js',
  devtool: 'eval',
  output: {
    path: path.join(__dirname, '/public'),
    filename: 'bundle.js',
    // 1. ++++ add the public path
    // to let webpack know
    // where to get the additional
    // bundles from
    publicPath: '/public/'
  },
  devServer: {
    publicPath: '/public/',
    historyApiFallback: true
  },
  
  ...

Async loader component

In App.js we want to load Details asynchronously (​return <Details show={shows[0]} {...props} />​)

We need to create a new component, AsyncRoute.js. This will just show a loading state while we're loading the component and then show the component. 
It's a higher order component, because it does not render anything to the DOM but encapsulates behaviour. 
  // ****   AsyncRoute.js   ****

import React from 'react'
const { object } = React.PropTypes

const AsyncRoute = React.createClass ({
  propTypes: {
    props: object,
    loadingPromise: object
  },
  getInitialState () {
    // because async loading routes
    return {
      loaded: false
    }
  },
  componentDidMount () {
    // doing it with a Promise
    // the future value of the promise 
    // is our component
    this.props.loadingPromise.then((module) => {
      // we don't put it on State
      // but on AsyncRoute
      // because it's immutable
      // we won't change it once it's loaded
      // don't forget to add .default, 
      // because it's from ES6 modules
      this.component = module.default
      // we only add loaded to the state
      // so that on re-render it will only check this
      // one property and not
      // a potentially big component (performance)
      // (see below in render function)
      this.setState({loaded: true})
    })
  },
  render () {
    if (this.state.loaded) {
      // if we loaded the module succesfully
      // instantiate this component
      // JSX will resolve `this.component`
      // we also pass down the props to this component
      // this lets us use this component with any route
      // (this is a higher order component)
      return <this.component {...this.props.props} />
    } else {
      // if not, the loader...
      return <h1>loading...</h1>
    }
  }
})

// don't forget to export
export default AsyncRoute
1:05:47

Let's use this with a component

In App.js, don't import Landing, but use AsyncRoute:
  // ****   App.js    ****

import React from 'react'
import { BrowserRouter, Match } from 'react-router'
import { Provider } from 'react-redux'
import store from './store'
// 1. ++-- use AsyncRoute instead of Landing
import AsyncRoute from './AsyncRoute'
import Search from './Search'
import Details from './Details'
import preload from '../public/data.json'
// 2. ++++ if running in Node
// (global exists only in Node)
if (global) {
  // then create a noop import function
  // so that it doesn't fail there
  global.System = { import () {} }
}

const App = () => {
  return (
    <Provider store={store}>
      <div className='app'>
        <Match
          exactly
          pattern='/'
          // 3. ++--
          // Replace landing here by AsyncRoute
          // pass props from Match to Asyncroute
          // System.import see below
          component={(props) => <AsyncRoute props={props} loadingPromise={System.import('./Landing')} />}
        />
        <Match
          pattern='/search'
          component={(props) => <Search shows={preload.shows} {...props} />}
        />
        <Match
          pattern='/details/:id'
          component={(props) => {
            const shows = preload.shows.filter((show) => props.params.id === show.imdbID)
            return <Details show={shows[0]} {...props} />
          }}
        />
      </div>
    </Provider>
  )
}

export default App

System.import('./Landing')

This is an async way of doing imports. This creates a Promise, and when it's fulfilled, we get our module (component). So Asyncroute will show this resolved Component.

So in practice, React will not request Landing immediately, but only when it's requested. Even the dependencies in the async component will only be downloaded with the component, not before. For example if axios is only used in Landing, it will only be downloaded with this component. 
Webpack will cut off the entire dependency graph. 

Q&A: ​getComponent​ is the old React Router way of doing async loading

Now Webpack automatically creates several bundles

Now we have two bundles. Webpack recognises ​System.import​ and cuts off the bundle. 
The cut off (async) bundle is only 3.19Kb big, so it's not a huge saving. It of course depends on what you can and do cut off.

Update package.json
  // ****   package.json
...
  "scripts": {
    "lint": "eslint js/**/*.js webpack.config.js",
    "build": "webpack",
    "dev": "webpack-dev-server",
    "watch": "npm run build -- --watch",
    "test": "NODE_ENV=test jest",
    "update-test": "npm run test -- -u",
    "cover": "npm run test -- --coverage",
    // 1. ++++
    "start": "NODE_ENV=server node server.js"
  },
...
​$ npm run start​ 

Now you have http://localhost:5050 (gives an error in fact bc forgot to export AsyncRoute).

Demoes that 0.bundle.js created by Webpack is only and only loaded when he navigates to the loading page, but not anywhere else. 

1:16:30

nodemon for auto NodeJS restarts

Restarts your NodeJS app when a file changes. 
example: ​$ NODE_ENV=server nodemon server.js
You can use watchmen as well or many other modules.

Do the code splitting for Search and Details

Only need to update App.js, not the components
  // ****   App.js    ****

import React from 'react'
// 4. ---- Remove BrowserRouter from the imports
// just keep Match
import { Match } from 'react-router'
import { Provider } from 'react-redux'
import store from './store'
import AsyncRoute from './AsyncRoute'
// 1. ---- remove importing of Details and Search
// Webpack will then automatically remove them
// from the bundle
import preload from '../public/data.json'
if (global) {
  global.System = { import () {} }
}

const App = () => {
  return (
    <Provider store={store}>
      <div className='app'>
        <Match
          exactly
          pattern='/'
          component={(props) => <AsyncRoute props={props} loadingPromise={System.import('./Landing')} />}
        />
        <Match
        // 2. ++++ update for Search
          pattern='/search'
          component={(props) => <AsyncRoute
          // add preload.shows to the props ↴
            props={Object.assign({shows: preload.shows}, props)}
            loadingPromise={System.import('./Search')}
          />}
        />
        <Match
        // 3. ++++
          pattern='/details/:id'
          component={(props) => {
          // get the show
            const shows = preload.shows.filter((show) => props.params.id === show.imdbID)
            return <AsyncRoute
              props={Object.assign({show: shows[0]}, props)}
              loadingPromise={System.import('./Details')}
            />
          }}
        />
      </div>
    </Provider>
  )
}

export default App
Now we have 4 bundles:
  • bundle.js  963Kb
  • 0.bundle.js 54Kb
  • 1.bundle.js 54Kb
  • 2.bundle.js 49Kb

System.import('Modulename') parameter must be a String

So that Webpack recognises it a code splitting point. Cannot be a variable or reference. Webpack uses static analysis and doesn't actually run your code. 

1:27:00

Building for production to reduce bundle sizes

1:28:00

​$ NODE_ENV=production webpack -p​ 

Now the bundles got about 5% smaller... 
You would need to run it with uglify to reduce to an acceptable size. 

To create a production bundle:
  // ****   webpack.config.js   ****

const path = require('path')
// 1. ++++ import webpack
const webpack = require('webpack')

module.exports = {
  context: __dirname,
  entry: './js/ClientApp.js',
  // 3. ++++ someone suggests
  // devtool: 'cheap-module-source-map',
  // in fact, just remove devtool completely
  // to get a huge reduction in bundle size
  output: {
    path: path.join(__dirname, '/public'),
    filename: 'bundle.js',
    publicPath: '/public/'
  },
  devServer: {
    publicPath: '/public/',
    historyApiFallback: true
  },
  // 2. ++++ plugins
  // not needed finally, see the Q&A below
  plugins: [
    // just copied it from the webpack docs
    // not sure this first plugin is needed
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: true
      }
    })
  ]
...
Still, the bundles are quite big... (1:31:30)
Probably because we also include some data and many polyfills with BabelRc. 

Q&A: cheap-module-source-map for smaller bundle sizes ?

Tries removing all of ​devtool​: this makes a huge difference! The bundle sizes are 5x smaller. Just comment/remove ​devtool​.

Q&A at 1:36:20 : the ​-p​ flag will do the minification $ NODE_ENV=production webpack -p​ 

1:33:00

218Kb for React, Redux, Router, Axios, etc...

Use Preact to reduce the size even further

Drop-in compatible library to React that is only 3Kb (gzipped, compressed) compared to ~80Kb of React.
It does not include 
  • propTypes
  • React.createClass only ES6 classes
For another 5Kb compat-library (preact-compat) they support everything in React. 

Just need to update the Webpack config
  // ****   webpack.config.js   ****

...
  devServer: {
    publicPath: '/public/',
    historyApiFallback: true
  },
  resolve: {
    extensions: ['.js', '.json'],
    // 1. ++++ add alias to Preact
    // inside resolve 
    alias: {
      // the react compatibility library (100% compat)
      react: 'preact-compat',
      'react-dom': 'preact-compat'
    }
  },
  stats: {
    colors: true,
    reasons: true,
    chunks: true
  },
  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.js$/,
        loader: 'eslint-loader',
        exclude: /node_modules/
      },
      {
        test: /\.json$/,
        loader: 'json-loader'
      },
      {
        // 2. ++++ include the module 
        include: [
          path.resolve(__dirname, 'js'),
          // everythingt that comes from must 
          // be run through Babel
          path.resolve('node_modules/preact-compat/src')
        ],
        test: /\.js$/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              url: false
            }
          }
        ]
      }
    ]
  }
}
Preact is almost 3x smaller, and is even faster. 
It does not have mixins, but the React team is trying to deprecate it anyway because they're "awful". 

You would also need to modify things server-side, that he did not do for the workshop.

There's also Inferno which is even smaller and faster, but may not be as stable as Preact. 

He really recommends using Preact. 

Workshop completed!!! 🙏🙇

Q&A: Started kits

Create React App (​$ npm install --global create-react-app​) by the React team. Takes care all of the tooling we covered + hot module reload. You have zero configuration options.

​$ create-react-app test

Sets up everything for you. Then later you can run ​$ npm run eject​ and it removes itself from the project (but leaves everything else there). 

1:46:00

It shows what the React (Facebook) team is doing and using currently (very up to date). Shows the best practices. 

Q&A: Code organisation advice

React Router has some really great examples. 

React Boilerplate

Also includes everything. It abstracts too much for him. So you don't learn the details.