Skip to content

Commit 8d35428

Browse files
committed
refactor(config): split server and spa builds via config parallelization
By using `require` for node rendering and `import()` for browser rendering, we can pre-render everything but still allow SPA-like behavior after any given page has been loaded. This split also allows us to use the `CommonsChunkPlugin` for the browser bundles which are now only `150kb` a piece (`index` and `vendor`) even without replacing `react` and `react-dom` with `preact` or `inferno`. That replacement should drop the total down a huge amount (maybe even enough to only generate a single core bundle) and I believe there's a few other optimizations we can make as well (e.g. using `import()` for the voting application). Note that we now have to finish handling `import()` promises for client-side `render`ing and hit the various other `TODO`s needed to finish the port to pure `webpack`.
1 parent f697633 commit 8d35428

File tree

5 files changed

+121
-95
lines changed

5 files changed

+121
-95
lines changed

src/components/Site/Site.jsx

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,12 @@ Content.children = Content.children
4242
});
4343

4444
class Site extends React.Component {
45-
static defaultProps = {
46-
lazy: true
47-
}
48-
4945
state = {
5046
mobileSidebarOpen: false
5147
}
5248

5349
render() {
54-
let { location, lazy } = this.props;
50+
let { location } = this.props;
5551
let { mobileSidebarOpen } = this.state;
5652
let sections = this._sections;
5753
let section = sections.find(({ url }) => location.pathname.startsWith(url));
@@ -91,40 +87,24 @@ class Site extends React.Component {
9187
path={ page.url }
9288
render={ props => {
9389
let path = page.path.replace('src/content/', '');
94-
95-
// TODO: Use `import()` `LazyLoad` component with nprogress
96-
// Blocked by `SSGPlugin` issue with `import()`
97-
if ( lazy ) {
98-
// import(`../../content/${path}`)
99-
// .then(module => {
100-
// console.log(module);
101-
// })
102-
// .catch(error => {
103-
// console.log(error);
104-
// });
105-
106-
return (
107-
<React.Fragment>
108-
<Sidebar
109-
className="site__sidebar"
110-
currentPage={ location.pathname }
111-
pages={ this._strip(section ? section.children : Content.children.filter(item => (
112-
item.type !== 'directory' &&
113-
item.url !== '/'
114-
))) } />
115-
<Page
116-
{ ...page }
117-
content={ require(`../../content/${path}`) } />
118-
</React.Fragment>
119-
);
120-
121-
} else {
122-
return (
90+
let module = this.props.import(path);
91+
92+
return (
93+
<React.Fragment>
94+
<Sidebar
95+
className="site__sidebar"
96+
currentPage={ location.pathname }
97+
pages={ this._strip(section ? section.children : Content.children.filter(item => (
98+
item.type !== 'directory' &&
99+
item.url !== '/'
100+
))) } />
123101
<Page
124102
{ ...page }
125-
content={ require(`../../content/${path}`) } />
126-
);
127-
}
103+
content={ module instanceof Promise ? (
104+
'TODO: Use `LazyLoad` component with nprogress'
105+
) : module } />
106+
</React.Fragment>
107+
);
128108
}} />
129109
))}
130110
<Route

src/index.jsx

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
3-
import ReactDOMServer from 'react-dom/server';
4-
import { BrowserRouter, StaticRouter, Route } from 'react-router-dom';
3+
import { BrowserRouter, Route } from 'react-router-dom';
54
import Site from './components/Site/Site';
6-
import Favicon from './favicon.ico';
75

86
// TODO: Re-integrate <GoogleAnalytics analyticsId="UA-46921629-2" />
97
// Consider `react-g-analytics` package
@@ -12,40 +10,13 @@ import Favicon from './favicon.ico';
1210
if ( window.document !== undefined ) {
1311
ReactDOM.render((
1412
<BrowserRouter>
15-
<Route path="/" component={ Site } />
13+
<Route
14+
path="/"
15+
render={ props => (
16+
<Site
17+
{ ...props }
18+
import={ path => import(`./content/${path}`) } />
19+
)} />
1620
</BrowserRouter>
1721
), document.getElementById('root'));
1822
}
19-
20-
// Server Side Rendering
21-
export default locals => {
22-
let { assets } = locals.webpackStats.compilation;
23-
24-
return ReactDOMServer.renderToString(
25-
<StaticRouter location={ locals.path } context={{}}>
26-
<html>
27-
<head>
28-
<meta charset="UTF-8" />
29-
<meta name="theme-color" content="#2B3A42" />
30-
<meta name="viewport" content="width=device-width, initial-scale=1" />
31-
<title>{/* TODO */} | webpack</title>
32-
<meta name="description" content={''/* TODO */} />
33-
<link rel="icon" type="image/x-icon" href={ Favicon } />
34-
{ Object.keys(assets).filter(asset => /\.css$/.test(asset)).map(path => (
35-
<link key={ path } rel="stylesheet" href={ `/${path}` } />
36-
))}
37-
</head>
38-
<body>
39-
<div id="root">
40-
<Route
41-
path="/"
42-
render={ props => <Site { ...props } lazy={ false } /> } />
43-
</div>
44-
{ Object.values(locals.assets).map(path => (
45-
<script key={ path } src={ path } />
46-
))}
47-
</body>
48-
</html>
49-
</StaticRouter>
50-
);
51-
};

src/server.jsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Import External Dependencies
2+
import React from 'react';
3+
import ReactDOMServer from 'react-dom/server';
4+
import { StaticRouter, Route } from 'react-router-dom';
5+
6+
// Import Components
7+
import Site from './components/Site/Site';
8+
9+
// Import Images
10+
import Favicon from './favicon.ico';
11+
12+
// Define bundles (previously used `Object.values(locals.assets)`) but
13+
// can't retrieve from there anymore due to separate compilation.
14+
const bundles = [
15+
'/vendor.bundle.js',
16+
'/index.bundle.js'
17+
];
18+
19+
// Export method for `StaticSiteGeneratorPlugin`
20+
// CONSIDER: How high can we mount `Site` into the DOM hierarchy? If
21+
// we could start at `<html>`, much of this could be moved to the `Site`
22+
// component itself (allowing easier utilization of page data for title,
23+
// description, etc).
24+
export default locals => {
25+
let { assets } = locals.webpackStats.compilation;
26+
27+
return ReactDOMServer.renderToString(
28+
<StaticRouter location={ locals.path } context={{}}>
29+
<html>
30+
<head>
31+
<meta charSet="UTF-8" />
32+
<meta name="theme-color" content="#2B3A42" />
33+
<meta name="viewport" content="width=device-width, initial-scale=1" />
34+
<title>{/* TODO */} | webpack</title>
35+
<meta name="description" content={''/* TODO */} />
36+
<link rel="icon" type="image/x-icon" href={ Favicon } />
37+
{ Object.keys(assets).filter(asset => /\.css$/.test(asset)).map(path => (
38+
<link key={ path } rel="stylesheet" href={ `/${path}` } />
39+
))}
40+
</head>
41+
<body>
42+
<div id="root">
43+
<Route
44+
path="/"
45+
render={ props => (
46+
<Site
47+
{ ...props }
48+
import={ path => require(`./content/${path}`) } />
49+
)} />
50+
</div>
51+
{ bundles.map(path => (
52+
<script key={ path } src={ path } />
53+
))}
54+
</body>
55+
</html>
56+
</StaticRouter>
57+
);
58+
};

webpack.common.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const fs = require('fs');
22
const path = require('path');
33
const webpack = require('webpack');
44
const FrontMatter = require('front-matter');
5+
const CleanPlugin = require('clean-webpack-plugin');
56
const CopyWebpackPlugin = require('copy-webpack-plugin');
67
const ExtractTextPlugin = require('extract-text-webpack-plugin');
78
const DirectoryTreePlugin = require('directory-tree-webpack-plugin');
@@ -96,6 +97,7 @@ module.exports = (env = {}) => ({
9697
]
9798
},
9899
plugins: [
100+
new CleanPlugin('dist'),
99101
new ExtractTextPlugin({
100102
filename: '[chunkhash].css',
101103
allChunks: true,
@@ -131,7 +133,6 @@ module.exports = (env = {}) => ({
131133
output: {
132134
path: path.resolve(__dirname, './dist'),
133135
publicPath: '/',
134-
filename: '[name].[hash].js',
135-
libraryTarget: 'umd'
136+
filename: '[name].bundle.js'
136137
}
137138
})

webpack.prod.js

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,48 @@ const path = require('path');
33
const webpack = require('webpack');
44
const merge = require('webpack-merge');
55
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
6-
const CleanPlugin = require('clean-webpack-plugin');
76
const SSGPlugin = require('static-site-generator-webpack-plugin');
87

98
// Load Common Configuration
109
const common = require('./webpack.common.js');
1110

12-
// Export a production configuration
13-
module.exports = env => merge(common(env), {
11+
// ...
12+
const prod = {
1413
plugins: [
15-
new CleanPlugin('dist'),
14+
new UglifyJSPlugin(),
1615
new webpack.DefinePlugin({
1716
'process.env.NODE_ENV': JSON.stringify('production')
18-
}),
19-
new UglifyJSPlugin(),
20-
new webpack.optimize.CommonsChunkPlugin({
21-
name: 'vendor',
22-
chunks: ['vendor'],
23-
minChunks: (module) => {
24-
return module.context && module.context.includes('node_modules');
25-
}
26-
}),
27-
new SSGPlugin({
28-
crawl: true,
29-
globals: {
30-
window: {}
31-
}
3217
})
3318
]
34-
})
19+
}
20+
21+
// Export both SSG and SPA configurations
22+
module.exports = env => [
23+
merge(common(env), prod, {
24+
target: 'node',
25+
entry: {
26+
index: './server.jsx'
27+
},
28+
plugins: [
29+
new SSGPlugin({
30+
crawl: true,
31+
globals: {
32+
window: {}
33+
}
34+
})
35+
],
36+
output: {
37+
filename: 'server.[name].js',
38+
libraryTarget: 'umd'
39+
}
40+
}),
41+
merge(common(env), prod, {
42+
target: 'web',
43+
plugins: [
44+
new webpack.optimize.CommonsChunkPlugin({
45+
name: 'vendor',
46+
chunks: [ 'index' ]
47+
})
48+
]
49+
})
50+
]

0 commit comments

Comments
 (0)