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
- Rolling our own
- react-server https://github.com/redfin/react-server
- Electrode http://www.electrode.io/
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
- 708