How to be fast

A holistic approach

Assets
Architecture
Pipeline
Network

Assets

Before: 464KB

After: ~75KB

Then: 1.4KB... 

Thanks Service Worker!

Architecture

Before

Fully dynamic content

After

Maximally static content

Pipeline

Webpack + Babel

  • Tree-shaking
  • Aliasing to Preact
  • HtmlWebpackPlugin
  • OfflinePlugin
{
  "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'">

Network

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

Useful Tools

  • https://meowni.ca/font-style-matcher/
  • http://yellowlab.tools/
  • http://csstriggers.com

 

... Most important tool: the slowest device you have

Bonus

How to be Fast

By Oliver Turner

How to be Fast

A holistic view - from assets to infrastructure - of performance for PWAs A presentation for Code First: Girls given at Ticketmaster in 2016

  • 705