A holistic approach
Before: 464KB
After: ~75KB
Then: 1.4KB...
Thanks Service Worker!
Before
Fully dynamic content
After
Maximally static content
{
"presets": ["react", ["es2015", { "modules": false }]],
"env": {
"development": {
"presets": ["react-hmre"]
},
"production": {
"plugins": ["lodash"]
},
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
}
.babelrc
Tree-shaking
plugins: [
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new LodashModuleReplacementPlugin(),
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new ExtractTextPlugin('styles.css'),
// Required to pull in assets for Offline Plugin
new HtmlWebpackPlugin({
production: true,
inject: false,
template: './src/index.html'
}),
new OfflinePlugin({
ServiceWorker: {events: true},
AppCache: false
})
],
webpack.config.prod.js
Static, cached content + Service Worker
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'react': 'preact-compat',
'react-dom': 'preact-compat'
}
},
webpack.config.prod.js
React in development, Preact in production
module: {
loaders: [
{
test: /\.jsx?$/,
loaders: ['babel-loader'],
include: [
path.join(__dirname, 'src'),
path.join(__dirname, './node_modules/preact-compat')
]
},
{
test: /\.(jpg|jpeg|png)$/,
loaders: ['file-loader?name=static/[name].[hash].[ext]'],
include: path.join(__dirname, 'static')
},
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract({
fallbackLoader: 'style-loader',
loader: [
{loader: 'css-loader'},
{loader: 'postcss-loader'},
{loader: 'sass-loader'}
]
})
},
{
test: /\.svg$/,
loader: 'url-loader?limit=8192!svgo-loader'
}
]
}
webpack.config.prod.js
"scripts": {
"clean": "rimraf dist",
"webpack": "cross-env NODE_ENV=production webpack -p --config webpack.config.prod.js",
"assets": "copy static/* dist && copy favicon.ico dist && copy CNAME dist",
"build": "run-s clean webpack assets",
"start": "node devServer.js",
"lint:js": "eslint src --ext .js --ext .jsx",
"lint": "run-s lint:*",
"test": "jest",
"critical": "critical ./dist/index.html --inline --base ./dist -H ./dist/index.html > /dev/null",
"surge": "surge -p dist -d literate-army.surge.sh",
"gh-pages": "gh-pages -d dist",
"deploy": "run-s build critical gh-pages"
},
package.json
<link href="/styles.css" rel="stylesheet" as="style" onload="this.rel='stylesheet'">
Cloudflare aliased to Github Pages
ssl + h2
Before: 8.5 seconds
https://www.webpagetest.org/
chrome inspector
https://developers.google.com/speed/pagespeed/insights/
After: < 1.5 seconds (uncached)
https://www.webpagetest.org/
chrome inspector
Before
Chrome Lighthouse
http://webmanife.st
{
"dir": "ltr",
"lang": "en",
"name": "HOLY FUCK THE ELECTION",
"scope": "/",
"display": "browser",
"start_url": "/",
"short_name": "HFTE",
"theme_color": "#000",
"description": "Holy fuck. Now what?",
"orientation": "any",
"background_color": "transparent",
"related_applications": [],
"prefer_related_applications": false,
"icons": [
{
"src": "/favicon.ico",
"sizes": "16x16 24x24 32x32 48x48 64x64"
},
{
"src": "/static/apple-touch-icon.png",
"type": "image/png",
"sizes": "57x57"
},
{
"src": "/static/apple-touch-icon-57x57.png",
"sizes": "57x57",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-114x114.png",
"sizes": "114x114",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png"
}
]
}
static/app.webmanifest
After
Chrome Lighthouse
... Most important tool: the slowest device you have
Bonus