Single-page application loading time optimization. Powered by Webpack, React, Node.js and isomorphic-style-loader

We talk about JavaScript. Each month in Warsaw, Poland.

Speaker

Michał Janaszek

"Single-page application loading time optimization. Powered by Webpack, React, Node.js and isomorphic-style-loader"

2016-11-09

@michaljanaszek

  1. Webpack code splitting
    Required: webpack
  2. Load chunks in the background
    Required: webpack
  3. Server side rendering
    Required: webpack, node, react / angular 2 (?)
  4. Isomorphic-style-loader powered by CSS Modules
    Required: webpack, node, react, isomorphic-style-loader

0. Initial website

Server

app.use(express.static(path.join(__dirname, 'public')));

app.get('*', (req, res) => {
const html = `<html>
<head>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="root">Loading</div>
<script async src="/bundle.js"></script>
</body>
</html>`;
res.status(200).send(html);
});
0

Router

import Layout from './Containers/Layout';
import Home from './Containers/Pages/Home';
import Talks from './Containers/Pages/Talks';
import Gallery from './Containers/Pages/Gallery';

const routes = {
path: '/',
component: Layout,
indexRoute: { component: Home },
childRoutes: [
{ path: 'talks', component: Talks },
{ path: 'gallery', component: Gallery },
],
};

export default routes;
0

Layout

import './layout.scss';
import Menu from '../Menu';
import Footer from '../Footer';

const Layout = props => (
<div className="root">
<Menu />
<div className="container">
{props.children}
</div>
<Footer />
</div>
);

export default Layout;
0

Menu

import { Link } from 'react-router';
import './menu.scss';

const Menu = () => (
<div className="menu">
<Link className="menu--link" activeClassName="menu--link__active" to="/">
Home
</Link>
// ...
</div>
);

export default Menu;
0

Webpack

entry: path.resolve(__dirname, 'src', 'client.js'),
output: {
path: path.resolve(__dirname, 'build', 'public'),
filename: 'bundle.js',
},
module: {
loaders: [{
exclude: /node_modules/,
loader: 'babel',
}, {
test: [/\.scss$/, /\.css$/],
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader'),
}],
},
plugins: [ new ExtractTextPlugin('style.css', { allChunks: true }) ],
0

Home route

times 0
0

1. Webpack code splitting

(I don't need you now)

1 http://24.media.tumblr.com/tumblr_m9ye2p1UbS1ryl3dho1_500.gif

Router

import Layout from './Containers/Layout';
const HomePage = (nextState, cb) => {
require.ensure([], require => cb(null, require('./Containers/Pages/Home').default));
};
const TalksPage = (nextState, cb) => {
require.ensure([], require => cb(null, require('./Containers/Pages/Talks').default));
};
const GalleryPage = (nextState, cb) => { /* ... */ };

const routes = {
path: '/',
component: Layout,
indexRoute: { getComponent: HomePage },
childRoutes: [
{ path: 'talks', getComponent: TalksPage },
{ path: 'gallery', getComponent: GalleryPage },
],
};
1

Home route

times 1
1

2. Load chunks in the background

(One code to rule them all)

2 https://24.media.tumblr.com/6fd5de573bdecbc02d30024f60b745f6/tumblr_n0ya9mVKkL1t5z6j7o6_500.gif

From home to talks

times 2 initial
1

Router

import Layout from './Containers/Layout';
import backgroundLoader from './backgroundLoader';


const HomePage = (nextState, cb) => {
require.ensure([], require => cb(null, require('./Containers/Pages/Home').default));
};
const TalksPage = (nextState, cb) => { /* ... */ };
const GalleryPage = (nextState, cb) => { /* ... */ };

backgroundLoader(HomePage);
backgroundLoader(TalksPage);
backgroundLoader(GalleryPage);


const routes = { /* ... */ };
export default routes;
2

backgroundLoader

const queue = [];
let isWaiting = false;
const requestLoad = () => {
if (isWaiting || !queue.length) { return; }
const loader = queue.pop();
isWaiting = true;
setTimeout(() => {
loader(() => {}, () => {
isWaiting = false;
requestLoad();
});
}, 200);
};
export default loader => {
queue.push(loader);
requestLoad();
};
2

From home to talks

times 2
2

3. Server side rendering

(Vitruvian Man is programming)

3 https://upload.wikimedia.org/wikipedia/commons/a/a5/Vitruvian_jumping_jacks.gif

Server

import ReactDOMServer from 'react-dom/server';
import { Router, match, createMemoryHistory } from 'react-router';
import routes from './routes';

app.get('*', (req, res) => {
const history = createMemoryHistory(req.url);

match({ history, routes }, (error, redirectLocation, renderProps) => {

const body = ReactDOMServer.renderToString(<Router {...renderProps} />);

const html = `<html>
<head>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="root"> ${body} </div> <script async src="/bundle.js"></script>
</body>
</html>`;
res.status(200).send(html);
});
});
3

Webpack

const client = {
entry: 'client.js',
output: { /* client build destination */ },
target: 'web',
module: { loaders: [ ...commonLoaders, ...clientLoaders ] },
plugins: [ ...commonPlugins, ...clientPlugins ],
};
const server = {
entry: 'server.js',
output: { /* server build destination */ },
target: 'node',
module: { loaders: [ ...commonLoaders, ...serverLoaders ] },
plugins: [ ...commonPlugins, ...serverPlugins ],
externals: nodeExternals(),
};

module.exports = [ client, server ];
3

Home route

times 3
3

4. Isomorphic-style-loader powered by CSS Modules

(Anarchy in the CSS)

4 https://despora.de/camo/2206bbbd2d7fa9083150efdbed59c56b1c53e917/
68747470733a2f2f7365636861742e6f72672f75706c6f6164732f696d616765732f7363616c65645f66756c6c5f61363164303038643165326561393261383461322e676966
._2cr2 {
margin-right: auto;
margin-left: auto;
padding-left: 15px;
padding-right: 15px
}
@media (min-width: 768px) {
._2cr2 {
width: 750px
}
}
@media (min-width: 992px) {
._2cr2 {
width: 970px
}
}
@media (min-width: 1200px) {
._2cr2 {
width: 1170px
}
}
.Lkjc {
margin-left: -15px;
margin-right: -15px
}
._1Fxh, ._1hOf, ._2aAe,
._3F50, ._3hHr, ._3Lpa,
._11K6, ._14ul, .dCl_,
.IM3C, .PjtN, .Z13z {
float: left
}
.IM3C {
width: 100%
}
._1Fxh {
width: 91.66666667%
}
._14ul {
width: 83.33333333%
}
._14ul {
width: 83.33333333%
}
._2aAe {
width: 75%
}
.Z13z {
width: 66.66666667%
}
._3Lpa {
width: 58.33333333%
}
._3hHr {
width: 50%
}
._11K6 {
width: 41.66666667%
}

Menu

import { Link } from 'react-router';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './menu.scss';


const Menu = () => (
<div className= {s.menu} >
<Link className= {s.menuLink} activeClassName= {s.menuLinkActive} to="/">
Home
</Link>
// ...
</div>
);

export default withStyles(s)(Menu);
4

Server

const css = [];

const onInsertCss = (...styles) => { styles.forEach(s => css.push(s._getCss())); };

let body = ReactDOMServer.renderToString(
<WithStylesContext onInsertCss={onInsertCss}>

<Router {...renderProps} />
</WithStylesContext>

);
const html = `<html>
<head>
<style id="css">${css.join('')}</style>

</head>
<body>
<div id="root">${body}</div>
<script async src="/bundle.js"></script>
</body>
</html>`;
res.status(200).send(html);
4

Webpack

module: {
loaders: [
{
test: [/\.scss$/, /\.css$/],
loaders: [
'isomorphic-style-loader',
`css-loader?${DEBUG ? 'sourceMap&' : 'minimize&'}modules&localIdentName=
${DEBUG ? '[name]_[local]_[hash:base64:4]' : '[hash:base64:4]'}`,
'sass-loader'
],

},
// ...
]
},
4

Home route

times 4
4

Final

Comparison

times comparison

Learn more

  1. Addy Osmani Progressive Web Apps with React.js
    Part I — Introduction
    Part 2 — Page Load Performance
    Part 3 — Offline support and network resilience
    Part 4 — Progressive Enhancement
  2. Maxime Fabre Webpack your bags
  3. react-starter-kit React Starter Kit — isomorphic web app boilerplate
  4. app used in this presentation spa-optimization

See you next month at WarsawJS