The Pursuit of Performance in React – Why React? – Virtual DOM



The Pursuit of Performance in React – Why React? – Virtual DOM

0 0


react-performance-presentation

The Pursuit of Performance in React

On Github NicholasBoll / react-performance-presentation

The Pursuit of Performance in React

by Nicholas Boll

Who Am I?

  • @NicholasBoll
  • nicholas.boll@gmail.com
  • I'm a Software Engineer at LogRhythm
  • I've been doing web development for a long time

Performance context

RAIL performance model

  • Response - 100ms
  • Animation - 16ms
  • Idle - 50ms
  • Load - 1s

Why React?

  • Community
  • Functional concepts
  • NOT Speed

Composition

// components/article.js

const Heading = ({ text }) => <h1>{text}</h1>;
const Content = ({ text }) => <p>{text}</p>;
const Article = ({ heading, content }) => (
  <article>
    <Heading text={heading} />
    <Content text={content} />
  </article>
);

export default Article;
          

Higher-order Composition

// containers/article.js

import { connect } from 'react-redux';
import Article from '../components/Article';

export default connect(state => state)(Article);
          

Understanding performance in React

Virtual DOM

Is DOM slow?

MorphDOM proves it isn't

Reconciler

What is it?

It has a cost:

https://jsfiddle.net/Lgfvpt86/5/

Initial render is fast

Reconciling updates can be slow

How does Redux help?

It changes the root with connect HoC

Is immutability enough?

What happens if the data we're passing isn't "Smart"? Or what if we need to touch something small on many similar components?

Example: Table scrolling

Side-loading

We need to load the right data at the right level

MobX

There is a HoC for that

import {observer} from 'mobx-react';

const TodoView = observer(({todo}) => <div>{todo.title}</div>)
          

What if we made everything observable?

...including props.children?

Observable makes all props immutable references

No shouldComponentUpdate

Observe all the things

import * as React from 'react'
import { isObservable } from 'mobx'
import { observer } from 'mobx-react'

export const mx = (Object.keys(React.DOM)
  .reduce((tags, tag) => {
    tags[tag] = createWrapper(tag);
    return answer;
  }, {}): any)

function createWrapper(tag) {
  class ReactiveClass extends React.Component {
    static displayName = `mx.${tag}`;
    propKeys: string[];
    constructor(props){
      super(props);
      this.propKeys = Object.keys(props);
    }
    render() {
      const propValues = this.propKeys
        .reduce((answer, key) => {
          const value = this.props[key];
          if(isObservableArray(value)){
            answer[key] = value.peek();
          }
          else if(isObservable(value) && value.get){
            answer[key] = value.get();
          }
          else {
            answer[key] = value;
          }
          return answer;
        }, {});
      return React.createElement(tag, propValues);
    }
  }
  return observer(ReactiveClass);
}
          

Usage:

import * as React from 'react'
import { mx } from './utils/mxReact'

const Heading = ({ text }) => <mx.h1>{text}</mx.h1>;
const Content = ({ text }) => <mx.p>{text}</mx.p>;
const Article = ({ heading, content }) => (
  <mx.article>
    <Heading text={heading} />
    <Content text={content} />
  </mx.article>
);

export default Article;
          

The reconciler is fooled into thinking children references don't change

MobX is cool, but...

  • You give up Redux
  • You give up Immutability
  • It favors models - we like raw data
  • It isn't fast enough

Other observable solutions?

  • RxJS5 is pretty popular and does what we need

Observables

  • Think of an array of values over time
  • Lazily evaluated
  • Can be cached
  • Redux store as normal - immutable state through observers
  • Tie into store.subscribe()
  • Create selectors from store (filters)
  • Everything else is data through computedProperty

computedProperty

// Take 1 or more observables, combine to to 1 result
const computedProperty = (...observables) =>
  Rx.Observable
    .combineLatest(...observables) // combine the last value of each observable
    .distinctUntilChanged() // only emit a value if it is different from previous
    .cache(1) // cache the last result if anyone asks...
          

Usage:

const a$ = Rx.Observable.of(1)
const b$ = Rx.Observable.of(2)

const c$ = computedProperty(
  a$,
  b$,
  (a, b) => a + b
)

c$.subscribe((c) => console.log(c)) // 3
          

getValue()

// get the last value to flow through a computed property
// synchronous resolution of a value
const getValue = (computedProperty) => {
  let value
  rxValue.subscribe((x:T) => value = x).unsubscribe()
  return (value: any)
}
          

Usage:

const a$ = Rx.Observable.of(1)
const b$ = Rx.Observable.of(2)

const c$ = computedProperty(
  a$,
  b$,
  (a, b) => a + b
)

console.log(getValue(c$)) // 3
            

What about events?

  • We keep events as callbacks
  • We use computedProperty and getValue together to extract values
const list = ({ scrollTop$, onScroll }) => {

  // This only gets defined once because properties are immutable pipelines
  // that don't change references from one render to the next. No breaking
  // pure-render
  const onWheel = (event) => (
    onScroll(getValue(scrollTop$) + event.wheelDeltaY)
  )

  const style$ = computeProperty(
    scrollTop$,
    (scrollTop) => {
      return { transform: `transform: translate3d(0px, ${scrollTop}px, 0px);` }
    }
  )

  return (
    <rx.div
      class="scrollable-container"
      onWheel={onWheel}
      style={style$}
    >
      { /* reactive children */ }
    </rx.div>
  )
}
          

Note: What I didn't show was the reactive children - it is a bit complicated to get into now, children are a computedProperty of observer components. Ex: Rx.Observable.of([ ListItem1, ListItem2 ]). We have a helper that takes a projection component (Ex: ListItem) and an array of indexes and returns an observable of an array of projected components. Similar to { items.map((item, index) => <ListItem key={index} data={item} />) }

What do we get?

  • Side-loading by default
  • Performance by default
  • No wasted reconciler time (react-addons-perf says nothing is wasted)
  • Stateless components API!
  • No shouldComponentUpdate
  • No need for special performance tricks (pure render or direct DOM)

Downsides?

  • All components are now reactive - percolates through your code
  • Might be harder to integrate with other HoCs

Demo

Questions?

The Pursuit of Performance in React by Nicholas Boll