Frontend Training

3/1/19

2019 edition

Who we are 🙋‍♂️

Clayton Ferris

Doug Waltman

Charlie King

Agenda

101

  • React ⚛️
  • NextJS 🔥
  • MobX 👩‍🏫
  • Styled-Components 💅

102

  • Deep dive on Server Rendering 🧜‍♀️
  • Caching and Concurrency 🚀
  • Docker 🚢
  • Reverse Proxy and CDN 🌎

103

  • Rancher 🤠
  • Bug Reporting 🐛
  • APM, Newrelic and Datadog 🐶
  • Uptime, Logging 📈

What's missing?

  • Hooks (mostly)
  • Testing w/ Jest
  • Flow/Typescript
  • ESLint, Prettier
  • Circle CI
  • NPM libraries/publishing
  • Storybook
  • Lerna (Monorepo

101

  • React ⚛️
  • NextJS 🔥
  • MobX 👩‍🏫
  • Styled-Components 💅

React

React ⚛️

React is a library for creating user interfaces

  • Component based
  • Declarative
  • No templates, logic and UI co-located in Components
import React from "react";
import WmSdk from "@ghostgroup/wm-sdk";

function TodoItem(props) {
  return <div>{props.todo}</div>;
}

class TodoList extends React.Component {
  state = {
    todos: [
      {
        subject: "Give training class"
      }
    ]
  };

  async componentDidMount() {
    // fetch data
    const sdk = new WmSdk();
    const todos = await sdk.todos.fetchTodos();
    this.setState({ todos });
  }

  render() {
    return (
      <ul>
        {this.state.todos.map(todo => (
          <TodoItem todo={todo} />
        ))}
      </ul>
    );
  }
}

Why React at Weedmaps?

  • Smaller runtime than Angular 1.x
  • Faster than Angular 1.x
  • React Architecture results in less bugs*
  • Community support and longevity
  • Server Rendering

Server Rendering

  • Render full HTML sites to clients
  • Unlike traditional Server rendering (Rails, ASP.net, etc) React SSR runs the same JavaScript code on the server (NodeJS, or other) that runs on the client

The not-so-good parts

  • React has less opinions
  • Server Rendering is complicated

WM Blog

We tried rolling our own SSR Project.

What did we learn?

  • Webpack is a nightmare 😱
  • Routing has to live in two places 🙅🏽‍♂️
    • React-Router wasn't quite ready
  • SASS was troublesome 🤬

NextJS

NextJS 🔥

  • A convention driven "universal" React framework
  • Handles the "hard" parts of Server Rendering like data-fetching, client reconciliation and Routing

pages

Every JS file in the "page" directory becomes a route. e.g. detail.js -> /detail

components

The components directory contains shared components used in pages

lib

Contains all the other modules and utilities

Webpack/Babel

Others Considered

101

Requirements

Email App

WMW-100

Card

As a email user

I would like to see a list of emails

so I can see who is trying to contact me

 

Conversation

Initial page to render the list of emails. Styling optional, mock data OK.

 

Confirmation

WHEN viewing localhost

THEN I will see a list of my emails

 

WMW-101

Card

As a email user

I would like to open an email

so I can read the full contents

 

Conversation

A detail page so I can read the sender, subject and full body.

 

Confirmation

WHEN viewing localhost/detail?emailId=<id>

THEN I will see the requested email

 

Live Code

WMW-102

Card

As a email user

I would like a preview of the email body

so I get more context without clicking

 

Conversation

Add a presentational component for header and subheader to visually differentiate and use semantic HTML

 

Confirmation

WHEN viewing localhost

THEN I will see a body preview in the list

WMW-103

Card

As a email user

I would like to be able to star emails

so I can keep track of what is important

 

Conversation

Add a star component. Persistence of star state optional.

 

Confirmation

WHEN viewing localhost

THEN I will see a star by each email

AND I can click the start to toggle

Atomic Design

Brad Frost

Levels

  • Atoms
  • Molecules
  • Organisms
  • Templates / Layouts
  • Pages

Forms a DAG

Live Code

(Global) State Management

  • React Context
  • Redux
  • MobX
  • … and many more

Live Code

WMW-103

Card

As a email user

I would like the UI to not be hot garbage

so I enjoy using the application

 

Conversation

This ticket covers styling the application to match the comps.

 

Confirmation

WHEN viewing the app

THEN It will look beautiful

WMW-104

Card

As a email user

I would like there to be no FOUC

so the app doesn't feel jank

 

Conversation

This ticket covers collecting our styles and ensuring they are rendered properly on the server

 

Confirmation

WHEN viewing localhost

THEN I will see no flash of un-styled content

The /path/ to styled-components

SASS

BEM

Styled

Y U No SASS and BEM?

  • To avoid a sea of dead styles
  • To remove our dependency on sass-loader and css-loader babel plugins and headaches therein
  • To improve our specificity management

So Y U Styled Components?

  • Tagged templates are so dreamy
  • Passing props is super helpful
  • Hashed IDs help us preserve specificity
  • SSR and matching IDs

<style data-styled-components>
  /* sc-component-id: email-detail__Detail-sc-1rhtos4-0 */
  .lgAiXo{background:#ccc;color:#333;padding:1rem;}
</style>

<div className="lgAiXo">

  Hello Weed
</div>

Identifiers

Live Code

Questions?

102

  • Deep dive on Server Rendering 🧜‍♀️
  • Caching and Concurrency 🚀
  • Docker 🚢
  • Reverse Proxy and CDN 🌎

102

Requirements

WMW-105

Card

As a Weedmaps engineer

I would like the email app to serve more requests

so it doesn't fall over during high load

Conversation

Add Concurrency to our email Application using PM2. This should be setup to use all CPUs - 1

Confirmation

WHEN viewing localhost

THEN the application works as normal

WHEN viewing the application processes

THEN It will be using more than 1

WMW-106

Card

As a email user

I would like the email app to load faster

so I can get my work done and get on with my day

Conversation

Add Caching with Memcached to the email application so page loads are super quick

Confirmation

WHEN viewing localhost

WHEN it is the first request

THEN the app will work as normal and there will be a cache MISS header

 

WHEN viewing localhost

WHEN it is the second request

THEN the app will work as normal and there will be a cache HIT header

Server Rendering

What is SSR?

Server Side Rendering in the React context is rendering your React Application on a Server, then sending the HTML to a client browser

const express = require("express");
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const Page = require("./pages/index");


function handler(req, res, next) {
  res.type('html')
     .send(ReactDOMServer.renderToString(<Page />));
  next();
}


const expressApp = express();
expressApp.use(handler).listen(3000);

Is that it?

Well no. The client side HTML must be "re-hydrated" with the same data and application state.

 

What are the forms of state we have to re-hydrate?

Node

JS

Data

Html

Render

Hydrate

Browser

Advantages

  • SEO
  • Time to first Contentful Paint

SEO

  • SEO in Single Page Apps is broken
  • Google might crawl, but Bing, Facebook, and other types of bots won't
  • Weedmaps used "Rendering as a Service" Pre-render to get around this.

Ionic

Prerender

Convergence

Is Mobile? Is ionic? Is bot?

TTFCP

Time to first Contentful Paint (TTFCP). Server side rendering makes this trivial to get content to users eyes.

Moonshot ~1s

Ionic ~3-4s

Data "Dehydration"

  • HTML and JS handled by NextJS
  • Data serialization kind of handled by NextJS. How does it work in practice?

Moonshot Render Cycle

  • 1st phase - Perform data fetching on Server
  • 2nd phase - Dehydrate on Server
  • 3rd phase - Rehydrate on Server
  • 4th phase - Render on Server
  • 5th phase - Rehydrate on Client
  • 6th phase - Render on Client
import { inject, observer } from 'mobx-react';


export class Page {
  static async getInitialProps(ctx, store) {
    // Call some store actions
    await store.fetchData();
    
    // Serialize the Store state
    const storeInitialState = store.dehydrate();

    // Return as props
    return { storeInitialState }; 
  }

  constructor() {
   // On Page creation, rehydrate the store
   this.props.store.rehydrate(this.props.storeInitialState);
  }

  render() {
    // Render the page with the Store Data
    return (<div>{this.props.store.someData}</div>);
  }
}

export default inject('store')(observer(Page));

Downsides

  • Moving from Single-tenancy to Multi-tenancy === 📈💰
  • 6 phase render cycle? More complex
  • Time to first byte (TTFB) is higher
  • Time To Interactive suffers

TTI

Why does Time to Interactive Suffer?

  • Because of a high Time to First Contentful Paint

SSR Best Practices

Waterfall requests

Avoid making multiple calls in serial

import React from "react";

export class MySlowPage extends React.Component {
  static async getInitialProps(ctx, store) {
    const user = await store.getUser(); // 200ms
    const emails = await store.fetchEmails(); // 200ms
    const folders = await store.fetchEmailFolders(); // 200ms

    return { user, emails, folders }; // 600ms render!
  }

  render() {
    return <div>Hello</div>;
  }
}

export class MyFastPage extends React.Component {
  static async getInitialProps(ctx, store) {
    const userPromise = store.getUser(); // 200ms
    const emailsPromise = store.fetchEmails(); // 200ms
    const foldersPromise = store.fetchEmailFolders(); // 200ms
    const [user, emails, folders] = Promise.all([
      userPromise,
      emailsPromise,
      foldersPromise
    ]);

    return { user, emails, folders }; // ~200ms render!
  }

  render() {
    return <div>Hello</div>;
  }
}

Too much data

Remember the double payload cost

import React from "react";

export class MyHeavyPage extends React.Component {
  static async getInitialProps(ctx, store) {
    const userPromise = store.getUser(); // 200ms
    const emailsPromise = store.fetchEmails(); // 200ms
    const foldersPromise = store.fetchEmailFolders(); // 200ms
    const [user, emails, folders] = Promise.all([
      userPromise,
      emailsPromise,
      foldersPromise
    ]);

    return { user, emails, folders }; // ~200ms render!
  }

  render() {
    return <div>Hello</div>;
  }
}
import React from "react";

export class MyLightPage extends React.Component {
  static async getInitialProps(ctx, store) {
    const userPromise = store.getUser(); // 200ms
    const emailsPromise = store.fetchEmails(); // 200ms
    const [user, emails] = Promise.all([
      userPromise,
      emailsPromise,
    ]);

    return { user, emails }; // ~200ms render!
  }

  async componentDidMount() {
    // Let's fetch the folder client side, 
    // since they aren't required to deliver a 
    // good experience
    const { store } = this.props;
    await store.fetchEmailFolders();
  }

  render() {
    return <div>Hello</div>;
  }
}

Too much JS

Remember the TTI Cost

import React from "react";
import leftPad from "left-pad";

export class MyHeavyPage extends React.Component {
  static async getInitialProps(ctx, store) {
    const userPromise = store.getUser();
    const emailsPromise = store.fetchEmails();
    const [user, emails] = Promise.all([
      userPromise,
      emailsPromise,
    ]);

    return { user, emails };
  }

  render() {
    return <div>{leftPad("Hello", 5)}</div>;
  }
}
import React from "react";

export class MyLightPage extends React.Component {
  static async getInitialProps(ctx, store) {
    const userPromise = store.getUser();
    const emailsPromise = store.fetchEmails();
    const [user, emails] = Promise.all([
      userPromise,
      emailsPromise,
    ]);

    return { user, emails };
  }

  render() {
    return <div>{'Hello'.padStart(5, ' ')}</div>;
  }
}

NodeJS Architecture

Live Code

Advanced, production process manager for Node.js

 

  • Configurable via process.yml or ecosystem.config.js(on)
  • Cluster mode
  • PM2-runtime acts as a replacement for node
  • Log merging
  • Memory limits and process restarting
  • … and so many more features we don't use

Your friendly neighborhood memory cache daemon.

 

  • Simple Key/Value Store
  • Meant to be as fast and lock-friendly as possible – O(1)
  • Forgetting is a Feature
  • Docker locally and in lower environments
  • ElastiCache in production

WMW-107

Card

As a Weedmaps engineer

I would like the email app to serve as little requests as possible

so it doesn't fall over during high load

Conversation

Add NGINX to our Application to handle serving static files so the application doesn't have to

Confirmation

WHEN viewing localhost

THEN static files will be served by NGINX

WMW-108

Card

As a email user

I would like the email app to use less bandwidth

so my data plan isn't destroyed

Conversation

Add GZIP to the NGINX server to ensure our JS bundles are smaller over the wire

Confirmation

WHEN viewing localhost

THEN static files will be served with GZIP encoding

 

Without a reverse proxy

NodeJS

JS / HTML / Images

With a reverse proxy

NodeJS

JS / Images

NGINX

HTML

Weedmaps Reverse Proxy

Exchange

JS / Images

Convergence

HTML

Moonshot

Logistics FE

HTML

HTML

What do we gain?

  • NGINX is great at serving Static Content
  • Our Node app should be focused on serving  Dynamic Content
  • Our reverse proxy frees up our Node app to focus on Dynamic content
  • NGINX can also protect our App with things like GZIP, Rate Limiting, Caching etc.

Live Code

Content Delivery Network (CDN)

Without a CDN

NodeJS

NGINX

Irvine

New Jersey

Frankfurt

Oregon (AWS US-west)

200ms

236ms

284ms

With a CDN

NodeJS

NGINX

Irvine

New Jersey

Frankfurt

Oregon (AWS US-west)

200ms

200ms

200ms

CDN

CDN

CDN

Other Benefits

  • Firewall (AWS WAF)
    • DDOS Protection at Edge
  • Global Rate limit (2000/req per second)
  • SSL Termination
  • Many more..

Questions?

103

103

Requirements

WMW-109

Card

As a Weedmaps engineer

I would like the email app to be live in production

so we can launch it to customers

Conversation

Add the required files to deploy the email app to Production. Use Nectar Collector to ship to the first environment

Confirmation

WHEN viewing the email app URL

THEN it will serve the email application

What is Rancher?

Rancher is our "Container Management Platform" or in other words, Docker Container Orchestration.

What is Container Orchestration?

  • Provisioning and deployment
  • Redundancy and availability
  • Scaling up or distributing evenly across host infrastructure
  • Movement of containers from one host to another if there is a shortage of resources in a host, or if a host dies
  • Allocation of resources between containers (Volumes)
  • Exposure of services running in a container
  • Load balancing / service discovery
  • Health monitoring
  • Configuration (Environment)

What

Where/How/When

Docker Image

  • In Staging
  • On 3 hosts
  • with the URL
  • and this config
  • when port 3000 is responding

Rancher

Engineer

Frontend System Architecture

https://weedmaps.com/deals

AWS Cloudfront (CDN)

AWS Load Balancer

API Gateway

Convergence

App

App

Rancher

App

Live Code

Debugging

Measuring / Monitoring

Browser Debugging 🌎

Key tools

  • Console
  • Network
  • Storage (Cookies, Local)

Console

  • Any Errors? Warnings?
  • Run the app in mobile emulation
  • Hook up a real device or simulator.
    • Connect the Safari dev tools 🙈

Network

  • Do you see any non-200 response codes?
    • This could be a critical dependency
  • Did the right config load?
    • Check for a `/static/config.js` file.
    • Does it look correct?
  • Inspect the network timings

Network Headers/Timings

Network Headers/Timings

Honeybadger

Creating Actionable Errors

  • Errors should be acted on
  • By Weedmaps Engineers
  • Or by the End User
// User actionable Errors should set a UI state, so they can act upon it
class Page extends React.Component {

  componentDidCatch(error, info) {
    // Show an error state to user, let them self-correct
    this.setState({ hasError: true });
    // Optionally notify us, if this is truly un-expected
    global.Honeybadger.notify(error);
  }
}

// Non user-actionable errors should report to Weedmaps
export const async function SomeMiddleware(req, res, next) {
  try {
    const data = await fetchSomeData();
  } catch(e) {
    Honeybadger.notify(e);
  }
}

// Don't do this!
class Page extends React.Component {

  componentDidCatch(error, info) {
    logger.error(error);
  }
}

// Or This
export const async function SomeMiddleware(req, res, next) {
  try {
    const data = await fetchSomeData();
  } catch(e) {
    logger.error(e);
  }
}

What is New Relic?

  • APM - Application Performance Management
  • Transaction time and traces
  • Apdex score - Moonshot set to a 0.5s threshold
  • Error rate
  • Deployment events
  • Node VM stats (GC, memory & CPU usage, etc.)
  • Alerting

What is Datadog?

  • APM - we're not using it (yet?)
  • Metrics/Monitoring integrations
    • AWS
    • NGINX
    • New Relic
    • Uptime
    • Honeybadger
  • Log aggregation

Dashboards

Infrastructure

Monitors

Log Explorer

Questions?

Frontend Training 101/102/103

By Charles King

Frontend Training 101/102/103

2019

  • 704