Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Highly Scalable, Robust & Fast Single ...

Erik Grijzen
February 04, 2017

Building Highly Scalable, Robust & Fast Single Page Web Apps

Presented @ Netcentric Summit 2017
Lisbon, Portugal

Single page applications have become increasingly more complex over the past few years. The way we approach and design web applications have changed a lot. User interfaces have become even more interactive and often contain very complex asynchronous data flows. The performance has also become very critical because the end users are expecting a native-like experience.

We will look at some framework agnostic concepts that will help you build truly reusable UI components and manage the application state in a predictable way. As the application grows and changes over time, we will leverage some best practices and tools to ensure a robust design that scales well in terms of requirements and performance.

Erik Grijzen

February 04, 2017
Tweet

More Decks by Erik Grijzen

Other Decks in Technology

Transcript

  1. Components to compose your user interface. App View 1 View

    2 Component Component Component Component Component Component Component Component
  2. Presentational components. ﹡ Nearly all markup. ﹡ No app dependencies.

    ﹡ Receives data from parent. ﹡ Not aware of app state. ﹡ Rarely contain state.
  3. Views are typically the first layer of container components. App

    View 1 View 2 Component Component Component Component Component Component Component Component
  4. Reusing components with different data. App View 1 View 2

    Component Component Component Component Component Component Component Component
  5. Separate the presentational components from your app. / Project A

    / app / common / containers / users / products / ... / Component Library / components / accordion / button / input / label / message-box / radio-buttons / select-box / table / text-area / ...
  6. Showcasing / Testing with mock data. <message-box text={...} /> Container

    1 Project 1 <message-box text={...} /> Container 2 Living Style Guide <message-box text={...} /> Container 3 UI Testing
  7. Why split components up in two categories? ﹡ Reusability. ﹡

    Testability. ﹡ Easier to reason about. ﹡ Easier to distribute. ﹡ Separation of concerns. ﹡ Work in isolation.
  8. CSS files in which all class names and animation names

    are scoped locally. // Header.css .title { background-color: red; } // Header.js import styles from "./styles.css"; element.innerHTML = `<h1 class="${styles.title}"> An example heading </h1>`;
  9. The classes are dynamically generated, unique, and mapped to the

    component. // bundled css .Header__title_n6slD { background-color: red; } // rendered html <h1 class="Header__title_n6slD"> An example heading </h1>
  10. Production build. // bundled css .n6slD { background-color: red; }

    // rendered html <h1 class="n6slD"> An example heading </h1>
  11. Why use CSS Modules? ﹡ True encapsulation. ﹡ The power

    of JavaScript. ﹡ No specificity conflicts. ﹡ No naming conflicts. ﹡ No BEM/SUIT conventions. ﹡ Forces best practices. ﹡ Explicit dependencies. ﹡ Dead code elimination. ﹡ File size.
  12. Why use unidirectional data flow? ﹡ Separation of concerns. ﹡

    Easier to reason about. ﹡ More predictable. ﹡ Easy to keep UI in sync. ﹡ Debugging.
  13. The state of your whole application is stored a single

    store. { todos: [ { id: 3, completed: false, text: 'Three' }, { id: 2, completed: false, text: 'Two' }, { id: 1, completed: false, text: 'One' } ] }
  14. The only way to change the state is to emit

    an action. { type: 'ADD_TODO', text: 'Build my first Redux app' }
  15. Typically we use action creators for better reusability. export const

    ADD_TODO = 'ADD_TODO' export function addTodo(text) { return { type: ADD_TODO, text, } }
  16. To specify how the state tree is transformed by actions,

    you write pure reducers. import { ADD_TODO } from './actions' function todosReducer(state = [], action) { switch (action.type) { case ADD_TODO: return [...state, { text: action.text, completed: false }] default: return state } }
  17. Each reducer manages its own part of the global state.

    import { combineReducers } from 'redux'; const todoApp = combineReducers({ todosReducer, visibilityReducer, });
  18. Middleware example. const logger = store => next => action

    => { console.log('dispatching', action); let result = next(action); console.log('next state', store.getState()); return result }
  19. Adding middleware to the store. import { createStore, combineReducers, applyMiddleware,

    } from 'redux'; const todoApp = combineReducers(reducers); const store = createStore( todoApp, applyMiddleware(logger) );
  20. redux-promise example. export const LOAD_DATA = 'LOAD_DATA' export function load()

    { return { type: LOAD_DATA, payload: fetch('api/v1/data'), } }
  21. Data is passed from top to bottom. View 1 Component

    Component Component Component Component Component Component Component Component Component Component Tree
  22. Example container component. class PersonDetailsContainer { constructor() { this.personData =

    { firstName: 'John', lastName: 'Doe', } } changeFirstName() { this.personData.firstName = 'Jane'; } }
  23. Making a change always creates a new reference. import Immutable

    from 'immutable'; class PersonDetailsContainer { constructor() { this.personData = Immutable.Map( firstName: 'John', lastName: 'Doe', }); } changeFirstName() { this.personData.set('firstName','Jane'); } }
  24. Separate the bootstrap logic. App View 1 View 2 Component

    Component Component Component Component Component Component Component Component Component Component Component
  25. Views are easy split points. App View 1 View 2

    Component Component Component Component Component Component Component Component Component Component Component Component
  26. Loading routes on demand. [ { path: 'home', getComponent(location, cb)

    { System.import('./containers/Home') .then(loadRoute(cb)); }, }, { path: 'products', getComponent(location, cb) { System.import('./containers/Products) .then(loadRoute(cb)); }, }, { path: 'shopping-cart', getComponent(location, cb) { System.import('./containers/ShoppingCart') .then(loadRoute(cb)); }, }, ]
  27. Separate code that is unlikely to change. App View 1

    View 2 Component Component Component Component Component Component Component Component Vendor Polyfills Component Component Component Component
  28. Bundle common logic together. App View 1 View 2 Component

    Component Component Component Component Component Component Component Vendor Polyfills Component Component Component Component
  29. Frameworks are slow by default. Framework Size (min) Size (min

    + gzip) Ember 2.2.0 435kb 111kb Ember 1.13.8 486kb 123kb Angular 2 566kb 111kb Angular 2 + RxJS 766kb 143kb Angular 1.4.5 143kb 51kb React 0.14.5 + React DOM 143kb 40kb React 0.14.5 + React DOM + Redux 133kb 42kb React 15.3.0 + React DOM 139kb 23kb
  30. Benefits of this lighter approach? ﹡ No observable models. ﹡

    No data binding. ﹡ No templating system ﹡ Less framework features. ﹡ Less code. ﹡ Less memory usage. ﹡ Less computations.