React server-side rendering

written by devangelist on Januar 8, 2015 in Allgemein and Frameworks and Javascript and React with no comments

Wie bereits in der Einführung von React beschrieben, können React-Views sowohl auf dem Client, als auch auf dem Server gerendert werden. Durch den Virtuellen DOM ist das auch nicht besonders komplex. Etwas schwieriger wird es dann, wenn der State zwischen Client und Server synchronisiert werden soll.

Warum eigentlich Server Side Rendering

React Views bereits auf dem Server zu Rendern hat mehrere Vorteile:

  • Wenn das HTML der Seite vorgerendert wird, erhöht das die Performance im Browser, da nicht erst alles zusammengestückelt werden muss. Dazu kann die HTML Seite beispielsweise von einem CDN (Content Delivery Network) gecached werden und muss somit nicht bei jedem Aufruf neu erstellt werden.
  • Suchmaschinen können noch kein JavaScript interpretieren und nur den HTML Code lesen. Im Moment bleibt es also die einzige Möglichkeit alles für die Suchmaschine relevante auf dem Server zu rendern.

Statisches Markup rendern

Die renderToStaticMarkup Methode nimmt eine React-Komponente entgegen und gibt das HTML als String zurück. Dieser kann einfach mit einer Template Engine wie Handlebars gerendert werden:

<div>{{{ markup }}}</div>

Reactive Komponenten

Dass die Komponenten auf dem Server gerendert werden können ist schön und gut – aber leider reicht das in den meisten Fällen nicht aus. Die komplette Interaktion, wie event-binding oder prop und state Änderungen fehlen vorerst.

Erst wenn sich React bewusst wird, dass es die Komponenten auch auf dem Client gibt, kann mit diesen interagiert werden.

Die renderToString Methode gibt ebenfalls das HTML als String zurück, ermöglicht aber im Gegensatz zur statischen Variante die clientseitige Interaktion.

Das klassische Beispiel eines inkrementellen Counters:

var Counter = React.createClass({
    getInitialState: function() {
        return {
            count: this.props.initialCount
        };
    },

    _increment: function() {
        this.setState({ count: this.state.count + 1 });
    },

    render: function() {
        return <span onClick={this._increment}>
            {this.state.count}
        </span>;
    }
});

Diese Komponente kann wie folgt auf dem Server gerendert werden:

var React = require('react');
var Counter = React.createFactory(require("./counter"));
var http = require('http');

var counterHtml = React.renderToString(
    Counter({ initialCount: 3 })
);

http.createServer(function(req, res) {
  if (req.url == '/') {
    res.send('<div id="container">' + counterHtml + '</div>');
  }
}).listen(3000);

Wird die Webseite über http://localhost:3000/ aufgerufen, so zeigt der Browser die initial gesetzte Zahl 3 an.

Bei einem Klick auf das Span-Element, welches eigentlich einen Click-Event triggern sollte, passiert nichts. Das ist ganz einfach zu erklären: React kennt auf dem Client diese Komponente noch nicht und kann somit auch keine event-handlers binden oder ein re-rendering durchführen.

Die Komponente muss also auch im Browser erstellt werden um richtig auf Click-Events reagieren zu können:

var Counter = React.createFactory(require("./counter"));
React.render(Counter({ initialCount: 3 }), document.getElementById("container"))

Für dieses Beispiel wird davon ausgegangen, dass React.js sowie die Komponente im Browser bereits geladen wurden (später dazu mehr).

Innerhalb dieses Aufrufs steckt aber noch ein bisschen Magie: Wird die Komponente mit denselben props wie auf dem Server aufgerufen, so rendert sich diese nicht neu. React erkennt also, dass sich der DOM nicht geändert hat, sich aber in Zukunft ändern könnte und führt alle nötigen Schritte aus, um auch die Events zu binden.

Wenn Komponenten nicht neu gerendert werden, wirkt sich dies positiv auf die Performance aus.

Props synchronisieren

Das Prinzip hinter dem Synchronisieren von props ist recht einfach: Der Client muss dieselben props wie auch der Server der Komponente mitgeben:

// server.js
// ...
var props = { initialCount: 3 };
var counterHtml = React.renderToString(
    Counter(props)
);
// ...
  res.send(
      '<div id="container">' + counterHtml + '</div>' +
      '<script>' +
        'var Counter = React.createFactory(require("./counter"));' +
        'React.render(Counter(' + safeStringify(props) + '), document.getElementById("container"))' +
      '</script>'
  );

Hinweis: Die safeStringify Funktion erlaubt das sichere Einbetten von JSON in ein Script-Tag.

In Zeile 5 werden die props { initialCount: 3 } der Server-Komponente mitgegeben, in Zeile 12 dem Client.

Die props können auch in ein getrenntes script-Tag:

<script id="props" type="application/json">
    {{{ theProps }}}
</script>
<script>
    var props = JSON.parse(document.getElementById("props").innerHTML);
    // ...
</script>

Da das zweite Script Tag nun komplett unabhängig ist, kann es mit einem kleinen “Trick” auch direkt in die Counter.jsx:

if (typeof window !== 'undefined') {
    var props = JSON.parse(document.getElementById("props").innerHTML);
    React.render(Counter(props), document.getElementById("container"));
}

Noch einen Schritt weiter gedacht können die Props auch direkt in die render-Methode der Komponente:

render: function() {
    var json = safeStringify(this.props);
    var propStore = <script type="application/json"
        id="someId"
        dangerouslySetInnerHTML={{__html: json}}>
    </script>;

    return <div onClick={this._increment}>
        {propStore}
        {this.state.count}
    </div>;
}

Das Mischen der props in die render-Methode ist nicht besonders schön, hat allerdings den großen Vorteil, dass sich jeglicher Code für das server-side Rendern innerhalb der React Komponente befindet.

Komponenten in den Browser

Auch der Browser muss neben React die React Komponenten kennen. Um nicht jede Komponente getrennt zu laden, wird aus diesen mit Tools wie Browserify oder Webpack ein komplettes Bundle. Um noch einmal auf das (wirklich sehr rudimentäre) Beispiel zurückzukommen:

http.createServer(function(req, res) {
  if (req.url == '/') {
    // ...
  } else if (req.url == '/bundle.js') {
    res.setHeader('Content-Type', 'text/javascript')
    browserify()
      .require('./counter.js', {expose: 'counter'})
      .transform({global: true}, literalify.configure({react: 'window.React'}))
      .bundle()
      .pipe(res)
  }

Wie synchronisiert intern React die Props?

Eine mit renderToString auf dem Server gerenderte Komponente beinhaltet ein data-react-checksum Attribut:

<div data-reactid=".pxv0hfgr28" data-react-checksum="85249504">
  4
</div>

Ein kurzer Blick in den React Source Code (ReactServerRendering.js) verrät, was im Hintergrund passiert:

function renderToString(component) {
    ...
    return transaction.perform(function() {
      var componentInstance = instantiateReactComponent(element, null);
      var markup = componentInstance.mountComponent(id, transaction, emptyObject);
      return ReactMarkupChecksum.addChecksumToMarkup(markup);
    }, null);
    ...
}

Die addChecksumToMarkup Funktion erstellt eine Adler-32 Checksum vom HTML Markup der Komponente und hängt diese an die auf dem Server gerenderte Komponente an.

Wird die Komponente anschließend auf dem Client gerendert, testet die canReuseMarkup (ReactMarkupChecksum.js) Funktion auf ein re-rendering:

canReuseMarkup: function(markup, element) {
    var existingChecksum = element.getAttribute(
        ReactMarkupChecksum.CHECKSUM_ATTR_NAME
    );
    existingChecksum = existingChecksum && parseInt(existingChecksum, 10);
    var markupChecksum = adler32(markup);
    return markupChecksum === existingChecksum;
}

Fazit

Das Beispiel zeigt im Grunde nur, wie es funktionieren kann – nicht wie es zwingend gemacht werden muss.

Es gibt elegantere Möglichkeiten den Server mit dem Client zu synchronisieren, wie fluxible-app (Dehydration/Rehydration). Dabei werden mit den Methoden rehydrate und dehydrate Snapshots vom server-side State an den Browser gesendet und angewendet.

Shared Code zwischen Client und Server bringt auf jedem Fall viele Vorteile und solange Suchmaschinen kein mit JavaScript gerendertes HTML indizieren (wollen), ist Server-Side Rendering die einzige Möglichkeit.

React server-side rendering: 1 Stern2 Sterne3 Sterne4 Sterne5 Sterne 5,00 von 5 Punkte, 4 abgegebene Stimmen.
Loading ... Loading ...

About the Author

Roberto Bez ist passionierter Webentwickler und TechLead bei der HolidayCheck AG. Für Roberto bedeutet das Entwickeln nicht nur Arbeit, sondern auch Freude, Motivation und täglich neue, aufregende Herausforderungen. Besonders gerne setzt er sich mit neuen Webtechnologien sowie Datenbanken aller Art auseinander und versucht diese in die tägliche Anwendungsentwicklung miteinzubringen. Neben dem Entwickeln trifft man ihn gerne Abends beim Laufen oder im Sommer bei Mountainbike-Touren durch die schönen Berge Südtirols.