Getting ready for production

@MichalZalecki

michalzalecki.com

woumedia.com

Minification

...duh!

uglifyjs pdf.js -c -o pdf.min.js
new webpack.optimize.UglifyJsPlugin({
  minimize: true,
  comments: false,
})

OR

Gzip

Reduce response size by about 70%

Used by 69.3% of all the websites

HTML

CSS

JS

PNG

JPG

MP4

nginx

gzip on;

node/express

const compression = require("compression");


const app = express();

app.use(compression());

Tree-shaking

Thanks to ES6 modules syntax bundlers are able to determine which exports are unused

// src/helpers.js

export function toUpper(str) {
  return str.toUpperCase();
}

export function toLower(str) {
  return str.toLowerCase();
}
// src/app.js

import { toUpper } from "./common/helpers";

const root = document.querySelector("#root");
const name = prompt("What's your name?");

root.innerHTML = `
  <h1>Hello ${toUpper(name)}!</h1>
`;
// build/app.js

function toUpper(str) {
  return str.toUpperCase();
}

const root = document.querySelector("#root");
const name = prompt("What's your name?");

root.innerHTML = `
  <h1>Hello ${toUpper(name)}!</h1>
`;

rollup

// build/app.js

/* unused harmony export toLower */
function toUpper(str) {
  return str.toUpperCase();
}

function toLower(str) {
  return str.toLowerCase();
}

webpack 2

// build/app.js

function toUpper(str) {
  return str.toUpperCase();
}

webpack 2 + uglify

Code Splitting

Webpack is capable of on demand code splitting as for routes or predictable user behavior

<Route
  path="/users"
  getComponent={(_nextState, cb) => {
    System.import("./users/components/UsersPage")
      .then((UsersPage) => {
        cb(null, UsersPage.default);
      });
  }}
/>
   Asset     Size  Chunks             Chunk Names
0.app.js  1.26 kB       0  [emitted]  
  app.js   214 kB       1  [emitted]  main

Resource splitting

const ExtractTextPlugin =
  require("extract-text-webpack-plugin");

plugins: [
  new ExtractTextPlugin({
    filename: "styles.[hash].css",
    allChunks: true,
  }),
],

module: {
  rules: [
    { test: /\.css$/,
      loader: ExtractTextPlugin.extract({
        fallbackLoader: "style-loader",
        loader: "css-loader",
      }),
    },
  ],
},
DocumentsPage (uses helpers.js)
  -> 0.js (includes helpers.js)

UsersPage (uses helpers.js)
  -> 1.js (includes helpers.js)
entry: {
  commons: ["./src/common/helpers"],
  app: "./src/index",
},

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: "commons",
    filename: "commons.js",
  }),
],
DocumentsPage (uses helpers.js)
  -> 0.js (__webpack_require__)

UsersPage (uses helpers.js)
  -> 1.js (__webpack_require__)

  -> commons.js (includes helpers.js)

NODE_ENV=production

Server-Side Rendering

Perform initial render on the server making initial load

great again

  1. match on location

  2. preload state

  3. create store

  4. render to string

  5. send the response

app.get("*", async (req, res) => {
  match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (renderProps) {
      const preloadedState = await fetchStateFromYourAPI(req);
      const head = `
      <script>
          window.__PRELOADED_STATE__=${JSON.stringify(preloadedState)};
      </script>`;
      const store = createStore(rootReducer, preloadedState);
      const reactHtml = renderToString(
        React.createElement(Provider, { store }, routerContext(renderProps))
      );
      res.render("index", { head, reactHtml });
    } else {
      res.status(404).send("Not found");
    }
  });
});

Prerendering

The quickest and cheapest way to (only) make crawlers like your webapp

app.use(require("prerender-node"));

ServiceWorker

if ("serviceWorker" in navigator) {
  // Service worker registered
  navigator.serviceWorker.register("/sw.js")
    .catch(err => {
      // Service worker registration failed
    });
} else {
  // Service worker is not supported
}

Caching strategies

function cacheableRequestFailingToCache({ event, cache }) {
  return fetch(event.request)
    .then(throwOnError) // do not cache errors
    .then(response => {
      cache.put(event.request, response.clone());
      return response;
    })
    .catch(() => cache.match(event.request));
}

Cacheable request failing to cache

function cacheFailingToCacheableRequest({ event, cache }) {
  return cache.match(event.request)
    .then(throwOnError)
    .catch(() => fetch(event.request)
      .then(throwOnError)
      .then(response => {
        cache.put(event.request, response.clone());
        return response;
      })
    );
}

Cache failing to cacheable request strategy

function requestFailingToCache({ event, cache }) {
  return fetch(event.request)
    .catch(() => cache.match(event.request));
}

Request failing to cache strategy

function requestFailingWithNotFound({ event }) {
  return fetch(event.request)
    .catch(() => {
      const body = JSON.stringify({
        error: "Sorry, you are off-line. Please, try later." });
      const headers = { "Content-Type": "application/json" };
      const response = new Response(body, {
        status: 404, statusText: "Not Found", headers });
      return response;
    });
}

Request failing with not found strategy

HTTP/2

Performance improvement for free

  1. Single bidirectional TLS connection

  2. Server push

  3. Easier caching

  4. No "up to 6 connections" limit

  5. Supported in all major browsers

Concatenation and sprites

Requires HTTPS, yeah!

listen 443 ssl http2;
listen [::]:443 ssl http2;

server_name example.com www.example.com;

ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
GoogleChrome/simplehttp2server

Thank you!

Getting ready for production

By Michał Załęcki

Getting ready for production

  • 1,601