Day 2, Segment 4, Part 3, Code splitting
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 loadingNow 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
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.