Angular and React

Friends learning from each other

Yet another VS talk?

Definitely not.

"Angular (v2) is closer to React than it is to AngularJS."

— Oliver Zeigermann

Three Scenarios

  • Runtime Performance Tuning - How to make it faster?
  • Server-side Rendering - Make it run on the server
  • 3rd-Party Library Integration - Render an SVG chart using 3rd-party lib

Runtime Performance Tuning

Example: SVG Boxes

<svg>
  <g>




  </g>
</svg>
<svg>
  <g>
    <rect width="10" height="10" x="..." y="...">
    <rect width="10" height="10" x="..." y="...">
    <rect width="10" height="10" x="..." y="...">
    ...
  </g>
</svg>
function drawBox(box) {
  const rect =
    document.createElementNS('...', 'rect');
  rect.setAttribute('width', 10);
  rect.setAttribute('height', 10);
  rect.setAttribute('x', box.x);
  rect.setAttribute('y', box.y);
  ...
}
function drawBox(box) {
  const rect =
    document.createElementNS('...', 'rect');
  rect.setAttribute('width', 10);
  rect.setAttribute('height', 10);
  rect.setAttribute('x', box.x);
  rect.setAttribute('y', box.y);
  ...
}
function drawBox(box) {
  const rect =
    document.createElementNS('...', 'rect');
  rect.setAttribute('width', 10);
  rect.setAttribute('height', 10);
  rect.setAttribute('x', box.x);
  rect.setAttribute('y', box.y);
  ...
}
function createBoxes(amount) {
  for (let i = 0; i < amount; i++) {
    const x = getRandomInt(0, 500);
    const y = getRandomInt(0, 500);
    const box = {
        x,
        y
    };
    drawBox(box);
  }
}
function createBoxes(amount) {
  for (let i = 0; i < amount; i++) {
    const x = getRandomInt(0, 500);
    const y = getRandomInt(0, 500);
    const box = {
        x,
        y
    };
    drawBox(box);
  }
}
function createBoxes(amount) {
  for (let i = 0; i < amount; i++) {
    const x = getRandomInt(0, 500);
    const y = getRandomInt(0, 500);
    const box = {
        x,
        y
    };
    drawBox(box);
  }
}
function createBoxes(amount) {
  for (let i = 0; i < amount; i++) {
    const x = getRandomInt(0, 500);
    const y = getRandomInt(0, 500);
    const box = {
        x,
        y
    };
    drawBox(box);
  }
}
createBoxes(5000);

svg = document.getElementById('svg');
svg.addEventListener('mousedown', onMouseDown);
svg.addEventListener('mouseup', onMouseUp);
svg.addEventListener('mousemove', onMouseMove);
createBoxes(5000);

svg = document.getElementById('svg');
svg.addEventListener('mousedown', onMouseDown);
svg.addEventListener('mouseup', onMouseUp);
svg.addEventListener('mousemove', onMouseMove);
function onMouseMove(event) {
  if (currentBox != null) {
    currentBox.setAttribute('x', event.clientX + offsetX);
    currentBox.setAttribute('y', event.clientY + offsetY);
  }
}
function onMouseMove(event) {
  if (currentBox != null) {
    currentBox.setAttribute('x', event.clientX + offsetX);
    currentBox.setAttribute('y', event.clientY + offsetY);
  }
}

React

export class App extends React.Component {


















}
export class App extends React.Component {
  constructor() {
    const boxes = [];
    // ... create boxes 
    this.state = { boxes: boxes, currentId: null };
  }













}
export class App extends React.Component {
  constructor() {
    const boxes = [];
    // ... create boxes
    this.state = { boxes: boxes, currentId: null };
  }

  render() {









  }
}
export class App extends React.Component {
  constructor() {
    const boxes = [];
    // ... create boxes
    this.state = { boxes: boxes, currentId: null };
  }

  render() {
    const {boxes} = this.state;








  }
}
export class App extends React.Component {
  constructor() {
    const boxes = [];
    // ... create boxes
    this.state = { boxes: boxes, currentId: null };
  }

  render() {
    const {boxes} = this.state;
    return (
      <svg onMouseDown={(event) => this.onMouseDown(event)}
           onMouseUp={(event) => this.onMouseUp(event)}
           onMouseMove={(event) => this.onMouseMove(event)}>
        <g>
          { boxes.map((box) => <Box box={box}/>) }
        </g>
    );
  }
}
export class App extends React.Component {
  constructor() {
    const boxes = [];
    // ... create boxes
    this.state = { boxes: boxes, currentId: null };
  }

  render() {
    const {boxes} = this.state;
    return (
      <svg onMouseDown={(event) => this.onMouseDown(event)}
           onMouseUp={(event) => this.onMouseUp(event)}
           onMouseMove={(event) => this.onMouseMove(event)}>
        <g>
          { boxes.map((box) => <Box box={box}/>) }
        </g>
    );
  }
}
export class App extends React.Component {
  constructor() {
    const boxes = [];
    // ... create boxes
    this.state = { boxes: boxes, currentId: null };
  }

  render() {
    const {boxes} = this.state;
    return (
      <svg onMouseDown={(event) => this.onMouseDown(event)}
           onMouseUp={(event) => this.onMouseUp(event)}
           onMouseMove={(event) => this.onMouseMove(event)}>
        <g>
          { boxes.map((box) => <Box box={box}/>) }
        </g>
    );
  }
}
export class Box extends React.Component {
  render() {
    const {box} = this.props;
    return <rect width="10" height="10"
                 x={box.x} y={box.y}/>;
  }
}
export class Box extends React.Component {
  render() {
    const {box} = this.props;
    return <rect width="10" height="10"
                 x={box.x} y={box.y}/>;
  }
}
export class Box extends React.Component {
  render() {
    const {box} = this.props;
    return <rect width="10" height="10"
                 x={box.x} y={box.y}/>;
  }
}
onMouseMove(event) {
  const {boxes} = this.state; 
  const box = boxes[this.state.currentId];
  box.x = event.clientX + this.offsetX;
  box.y = event.clientY + this.offsetY;
  this.setState({boxes});
}
onMouseMove(event) {
  const {boxes} = this.state; 
  const box = boxes[this.state.currentId];
  box.x = event.clientX + this.offsetX;
  box.y = event.clientY + this.offsetY;
  this.setState({boxes});
}
onMouseMove(event) {
  const {boxes} = this.state; 
  const box = boxes[this.state.currentId];
  box.x = event.clientX + this.offsetX;
  box.y = event.clientY + this.offsetY;
  this.setState({boxes});
}

Demo

How does React render?

React's change detection happens in Virtual DOM level

  • Every mouse move re-renders full application (all boxes)
  • Only difference between old and new Virtual DOM affects real DOM

Making it faster

React comes with a component lifecycle hook shouldComponentUpdate()

  • Gives us more control over when a component needs to be re-rendered
  • Immutable data structures can speed up model check
onMouseMove(event) {
  const {boxes} = this.state; 
  boxes[this.state.currentId] = {
    x: event.clientX + this.offsetX,
    y: event.clientY + this.offsetY
  };
  this.setState({boxes});
}
onMouseMove(event) {
  const {boxes} = this.state; 
  boxes[this.state.currentId] = {
    x: event.clientX + this.offsetX,
    y: event.clientY + this.offsetY
  };
  this.setState({boxes});
}
export class Box extends React.Component {





  render() {
    const {box} = this.props;
    return <rect width="10" height="10"
            x={box.x} y={box.y}/>;
  }
}
export class Box extends React.Component {

  shouldComponentUpdate(nextProps) {
    return this.props.box !== nextProps.box;
  }

  render() {
    const {box} = this.props;
    return <rect width="10" height="10"
            x={box.x} y={box.y}/>;
  }
}

Demo

Angular

@Component({
  template: `
    <svg (mousedown)="mouseDown($event)"
      (mouseup)="mouseUp($event)"
      (mousemove)="mouseMove($event)">

      <svg:g square-box
        *ngFor="let box of boxes"
        [box]="box">
      </svg:g>

    </svg>
  `
})
export class AppComponent implements OnInit {
  ...
}
@Component({
  template: `
    <svg (mousedown)="mouseDown($event)"
      (mouseup)="mouseUp($event)"
      (mousemove)="mouseMove($event)">

      <svg:g square-box
        *ngFor="let box of boxes"
        [box]="box">
      </svg:g>

    </svg>
  `
})
export class AppComponent implements OnInit {
  ...
}
@Component({
  template: `
    <svg (mousedown)="mouseDown($event)"
      (mouseup)="mouseUp($event)"
      (mousemove)="mouseMove($event)">

      <svg:g square-box
        *ngFor="let box of boxes"
        [box]="box">
      </svg:g>

    </svg>
  `
})
export class AppComponent implements OnInit {
  ...
}
@Component({
  template: `
    <svg (mousedown)="mouseDown($event)"
      (mouseup)="mouseUp($event)"
      (mousemove)="mouseMove($event)">

      <svg:g square-box
        *ngFor="let box of boxes"
        [box]="box">
      </svg:g>

    </svg>
  `
})
export class AppComponent implements OnInit {
  ...
}
@Component({
  selector: '[square-box]',
  template: `
    <svg:rect 
      [attr.x]="box.x"
      [attr.y]="box.y"
      width="10"
      height="10">
    </svg:rect>
  `
})
export class BoxComponent {
  @Input() box;
}
@Component({
  selector: '[square-box]',
  template: `
    <svg:rect 
      [attr.x]="box.x"
      [attr.y]="box.y"
      width="10"
      height="10">
    </svg:rect>
  `
})
export class BoxComponent {
  @Input() box;
}

Demo

Performance?

  • About as fast as React's unoptimized implementation
  • Can be improved - Angular checks every binding every single time

Making it faster

Angular's allows us to control a component's ChangeDetectionStrategy

  • Skips subtrees for change detection if set to OnPush and no inputs have changed
  • Works great with immutable data structures
@Component({
  selector: '[square-box]',
  template: `
    <svg:rect 
      [attr.x]="box.x"
      [attr.y]="box.y"
      width="10"
      height="10">
    </svg:rect>
  `

})
export class BoxComponent {
  @Input() box;
}
@Component({
  selector: '[square-box]',
  template: `
    <svg:rect 
      [attr.x]="box.x"
      [attr.y]="box.y"
      width="10"
      height="10">
    </svg:rect>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BoxComponent {
  @Input() box;
}

Demo

Result

  • OnPush saves 8 bindings per box
  • ngFor check every row, instead of just collection length (like React)
  • Fair setup: "simpleNgFor" that only checks length (big effort as not provided by core)

Observations

  • Unoptimized: comparable performance
  • Both can be optimized in similar ways to some degree
  • More optimization possible (at least in in Angular),
    requires more effort

Keep in mind: This was not about who's faster

Server-side Rendering

Motivations

There are many reasons to render an app on the server, some of them are:

  • First-Page-Impressions - Keep the user bei-Laune™
  • Preview - When sharing links on Social Media
  • SEO - Because web

Bonus: Application code can be shared between client and server!

Challenges

  • Universal - Make framework run on the server
  • State Transition - Server-side state needs to be transferred to client
  • No Flicker - do not re-render server rendered result if not necessary

SPA


Universal App


const server = express();

app.get('/', (req, res) => {
  const html = renderApp(5000);
  res.send(html);
});
          
app.listen(3000, () => console.log('Running...'));
const server = express();

app.get('/', (req, res) => {
  const html = renderApp(5000);
  res.send(html);
});
          
app.listen(3000, () => console.log('Running...'));
import App from '../common/App';
import { renderToString } from 'react-dom/server';

function renderApp(numberOfBoxes) {
  const boxes = createBoxes(numberOfBoxes);

  const html = renderToString(<App boxes={boxes}/>);

  return renderPage(html, boxes);
}
import App from '../common/App';
import { renderToString } from 'react-dom/server';

function renderApp(numberOfBoxes) {
  const boxes = createBoxes(numberOfBoxes);

  const html = renderToString(<App boxes={boxes}/>);

  return renderPage(html, boxes);
}
import App from '../common/App';
import { renderToString } from 'react-dom/server';

function renderApp(numberOfBoxes) {
  const boxes = createBoxes(numberOfBoxes);

  const html = renderToString(<App boxes={boxes}/>);

  return renderPage(html, boxes);
}
function renderPage(html, data) {
  return `
    <html>
      <body>

        <div id="mount">${html}</div>

        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(data)};
        </script>
        <script src="dist/main.js"></script>

      </body>
    </html>
  `;
}
function renderPage(html, data) {
  return `
    <html>
      <body>

        <div id="mount">${html}</div>

        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(data)};
        </script>
        <script src="dist/main.js"></script>

      </body>
    </html>
  `;
}
function renderPage(html, data) {
  return `
    <html>
      <body>

        <div id="mount">${html}</div>

        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(data)};
        </script>
        <script src="dist/main.js"></script>

      </body>
    </html>
  `;
}
// on the client...

const initialState = window.__INITIAL_STATE__;
const { boxes } = initialState;

const mountNode = document.getElementById('mount');
ReactDOM.render(<App boxes={boxes}/>, mountNode);
// on the client...

const initialState = window.__INITIAL_STATE__;
const { boxes } = initialState;

const mountNode = document.getElementById('mount');
ReactDOM.render(<App boxes={boxes}/>, mountNode);
// on the client...

const initialState = window.__INITIAL_STATE__;
const { boxes } = initialState;

const mountNode = document.getElementById('mount');
ReactDOM.render(<App boxes={boxes}/>, mountNode);

data-react-checksum

Guarentees that React does not re-render on client when not necessary

Demo

// browser.module.ts

import { BrowserModule } from '@angular/platform-browser';

@NgModule({
  bootstrap: [AppComponent],
  declarations: [AppComponent, SquareBoxComponent],
  imports: [BrowserModule]
})
export class AppModule {}
// browser.module.ts

import { BrowserModule } from '@angular/platform-browser';

@NgModule({
  bootstrap: [AppComponent],
  declarations: [AppComponent, SquareBoxComponent],
  imports: [BrowserModule]
})
export class AppModule {}
// node.module.ts





@NgModule({
  bootstrap: [AppComponent],
  declarations: [AppComponent, SquareBoxComponent],
  imports: []
})
export class AppModule {}
// node.module.ts

import { CommonModule } from '@angular/common';
import { UniversalModule } from 'angular2-universal/node';


@NgModule({
  bootstrap: [AppComponent],
  declarations: [AppComponent, SquareBoxComponent],
  imports: [CommonModule, UniversalModule]
})
export class AppModule {}
import { createEngine } from 'angular2-express-engine';
import { AppModule } from './app/node.module';
            
const app = express();

app.engine('.html', createEngine({ ngModule: AppModule });

app.get('/', (req, res) => {
  res.render('index', { req, res });
});
import { createEngine } from 'angular2-express-engine';
import { AppModule } from './app/node.module';
            
const app = express();

app.engine('.html', createEngine({ ngModule: AppModule });

app.get('/', (req, res) => {
  res.render('index', { req, res });
});
import { createEngine } from 'angular2-express-engine';
import { AppModule } from './app/node.module';
            
const app = express();

app.engine('.html', createEngine({ ngModule: AppModule });

app.get('/', (req, res) => {
  res.render('index', { req, res });
});

How to preserve state?

// node.module.ts
@NgModule({
  ...
  providers: [StateService]
})
export class AppModule {

  constructor(private state: StateService) {}

  universalDoDehydrate(universalCache) => {
    this.state.set('boxes', createBoxes(5000));

    const values = this.state.valuesAsJson()
    universalCache['StateService'] = JSON.stringify(values);
  }
}
// node.module.ts
@NgModule({
  ...
  providers: [StateService]
})
export class AppModule {

  constructor(private state: StateService) {}

  universalDoDehydrate(universalCache) => {
    this.state.set('boxes', createBoxes(5000));

    const values = this.state.valuesAsJson()
    universalCache['StateService'] = JSON.stringify(values);
  }
}
// node.module.ts
@NgModule({
  ...
  providers: [StateService]
})
export class AppModule {

  constructor(private state: StateService) {}

  universalDoDehydrate(universalCache) => {
    this.state.set('boxes', createBoxes(5000));

    const values = this.state.valuesAsJson()
    universalCache['StateService'] = JSON.stringify(values);
  }
}
// node.module.ts
@NgModule({
  ...
  providers: [StateService]
})
export class AppModule {

  constructor(private state: StateService) {}

  universalDoDehydrate(universalCache) => {
    this.state.set('boxes', createBoxes(5000));

    const values = this.state.valuesAsJson()
    universalCache['StateService'] = JSON.stringify(values);
  }
}
// node.module.ts
@NgModule({
  ...
  providers: [StateService]
})
export class AppModule {

  constructor(private state: StateService) {}

  universalDoDehydrate(universalCache) => {
    this.state.set('boxes', createBoxes(5000));

    const values = this.state.valuesAsJson()
    universalCache['StateService'] = JSON.stringify(values);
  }
}
// browser.module.ts

@NgModule({
  ...
  imports: [...],
  providers: [StateService]
})
export class AppModule {

  constructor(state: StateService) {
    const values = window[UNIVERSAL_CACHE]['StateService'];
    this.state.valuesFromJson(values);   
  }
}
// browser.module.ts

@NgModule({
  ...
  imports: [...],
  providers: [StateService]
})
export class AppModule {

  constructor(state: StateService) {
    const values = window[UNIVERSAL_CACHE]['StateService'];
    this.state.valuesFromJson(values);   
  }
}
// browser.module.ts

@NgModule({
  ...
  imports: [...],
  providers: [StateService]
})
export class AppModule {

  constructor(state: StateService) {
    const values = window[UNIVERSAL_CACHE]['StateService'];
    this.state.valuesFromJson(values);
  }
}
// browser.module.ts

@NgModule({
  ...
  imports: [..., UniversalModule],
  providers: [StateService]
})
export class AppModule {

  constructor(state: StateService, @Inject(UNIVERSAL_CACHE) cache) {
    const values = cache['StateService'];
    this.state.valuesFromJson(values);
  }
}
@Component({...})
export class AppComponent {

  boxes = [];

  constructor(private state: StateService) {}

  ngOnInit() {
    this.boxes = state.get('boxes');
  }
}
@Component({...})
export class AppComponent {

  boxes = [];

  constructor(private state: StateService) {}

  ngOnInit() {
    this.boxes = state.get('boxes');
  }
}

Demo

Observations

  • Angular
    • Feels more verbose due to NgModules
    • State transfer requires more effort, but uses DI
    • Supports server-side Http etc. out-of-the-box
    • Can be AoT compiled for even faster render

Observations

  • React
    • Pretty straight forward setup
    • Easy to prerender app and send to the client
    • Server-side WebAPI equivalents using isomorphic-fetch or Flux frameworks

3rd-Party Library Integration

Motivation

  • There are a lot of 3rd-party libs out there
  • Usually not written in framework of our choice
  • Examples: Bootstrap, d3, jQuery, ...

Example: NVD3 Pie Chart

Challenges

  • DOM Access - How to access DOM element?
  • Updates - Sending model updates to 3rd party lib
class Chart extends React.Component {

















}
class Chart extends React.Component {
  render() {
    return <svg ref={c => this.chart = c}></svg>
  }














}
class Chart extends React.Component {
  render() {
    return <svg ref={c => this.chart = c}></svg>
  }

  shouldComponentUpdate() {
    return false;
  }










}
class Chart extends React.Component {
  render() {
    return <svg ref={c => this.chart = c}></svg>
  }

  shouldComponentUpdate() {
    return false;
  }

  componentDidMount() {
    const data = calculateData(this.props.positions);
    createChart(this.chart, data);
  }





}
class Chart extends React.Component {
  render() {
    return <svg ref={c => this.chart = c}></svg>
  }

  shouldComponentUpdate() {
    return false;
  }

  componentDidMount() {
    const data = calculateData(this.props.positions);
    createChart(this.chart, data);
  }





}
class Chart extends React.Component {
  render() {
    return <svg ref={c => this.chart = c}></svg>
  }

  shouldComponentUpdate() {
    return false;
  }

  componentDidMount() {
    const data = calculateData(this.props.positions);
    createChart(this.chart, data);
  }

  componentWillReceiveProps(nextProps) {
    const data = calculateData(this.nextProps.positions);
    updateChart(this.chart, data);
  }
}

Demo

@Component({
  template: `<svg></svg>`
})
export class ChartComponent {















}
@Component({
  template: `<svg #chart></svg>`
})
export class ChartComponent {

  
  @ViewChild('chart') chart: ElementRef;










}
@Component({
  template: `<svg #chopchop></svg>`
})
export class ChartComponent {

  
  @ViewChild('chopchop') chart: ElementRef;










}
@Component({
  template: `<svg #chart></svg>`
})
export class ChartComponent implements AfterViewInit {

  @Input() positions;
  @ViewChild('chart') chart: ElementRef;

  ngAfterViewInit() {
    const data = calculateData(this.positions);
    createChart(this.chart, data);
  }





}
@Component({
  template: `<svg #chart></svg>`
})
export class ChartComponent implements AfterViewInit, DoCheck {

  @Input() positions;
  @ViewChild('chart') chart: ElementRef;

  ngAfterViewInit() {
    const data = calculateData(this.positions);
    createChart(this.chart, data);
  }

  ngDoCheck() {
    const data = calculateData(this.positions);
    updateChart(this.chart, data);
  }
}

Demo

Observations

  • 3rd-party integration very similar in both React and Angular 2
  • React allows for easy/immediate element reference access
  • Angular comes with ViewChild/ViewChildren (Observable-based)

Conclusion

Conclusion

  • React and Angular 2 provide similar solutions to our challenges
  • Learning to do one thing in one of them can teach you about the other
  • Both can be tuned to be reasonably fast in most realistic scenarios

Bottom Line: Angular 2 gives you the full stack, React gives you choices

Credits

This talk wouldn't have been possible without these fine people:

  • Patrick
  • Tobias
  • Wassim
  • Jeff

Thanks.

Friends learning from each other