Performance-Tuning ist ein wichtiger Bestandteil jedes App-Entwicklungsvorhabens. In diesem Artikel spreche ich über einige der Tools und Techniken, die wir verwendet haben, um Leistungsengpässe in der ReactXP-basierten Skype-App zu identifizieren und zu beheben.
Einer der Vorteile einer plattformübergreifenden Codebasis ist, dass viele Leistungsverbesserungen allen Plattformen zugutekommen.
Messung und Analyse
Man sagt, man kann nichts verbessern, was man nicht messen kann. Das gilt insbesondere für das Performance-Tuning. Wir verwenden eine Vielzahl von Tools, um festzustellen, welche Code-Pfade leistungsentscheidend sind.
Unabhängig vom Analyse-Tool möchten Sie möglicherweise die Produktionsversion der App verwenden, wenn Sie die Leistung messen. Der React JavaScript-Code führt viele aufwändige Laufzeitprüfungen durch, wenn er im „Entwicklermodus“ ausgeführt wird. Dies kann Ihre Messungen erheblich verzerren.
Chrome-Performance-Tools
Der Chrome-Browser bietet hervorragende Tools zum Nachzeichnen und Visualisieren. Öffnen Sie das Fenster „Entwicklertools“, klicken Sie auf die Registerkarte „Leistung“ und dann auf die Schaltfläche „Aufzeichnen“. Sobald Sie die Aufzeichnung beendet haben, zeigt Chrome eine detaillierte Zeitachse mit Aufrufhierarchien an. Zoomen Sie hinein und heraus, um festzustellen, wohin Ihre Zeit fließt.
Systrace
React Native bietet eine Möglichkeit, Systrace, eine Methode für methodenbasierte Trace-Aufzeichnungen, zu aktivieren und zu deaktivieren. Es zeichnet sowohl native als auch JavaScript-Methoden auf und bietet somit einen guten Überblick darüber, was in der gesamten App passiert. Um Systrace zu verwenden, erstellen und stellen Sie eine Entwicklerversion auf Ihrem Gerät bereit. Schütteln Sie das Gerät, um das Entwicklermenü anzuzeigen (oder drücken Sie Befehl-D, wenn Sie den iOS-Simulator ausführen). Wählen Sie „Systrace starten“, führen Sie dann die Aktion aus, die Sie messen möchten. Wenn Sie Systrace stoppen, wird eine HTML-Trace-Datei erstellt. Sie können diese Trace-Datei in Chrome visualisieren und mit ihr interagieren. Neuere Versionen von Chrome haben eine Funktion, die im Systrace-Code verwendet wird, als veraltet markiert. Sie müssen sie daher wie folgt bearbeiten. Fügen Sie einfach die folgende Zeile in den Kopfbereich der generierten HTML-Datei ein.
<script src="https://rawgit.com/MaxArt2501/object-observe/master/dist/object-observe.min.js"></script>
Konsolenprotokollierung
Die einfache Konsolenprotokollierung ist oft eine effektive Methode zur Leistungsverfolgung. Protokolleinträge können mit Zeitstempeln mit Millisekundenauflösung ausgegeben werden. Rufen Sie einfach Date.now() auf, um die aktuelle Zeit zu erhalten. Die Dauer von leistungsentscheidenden Operationen (wie dem App-Start) kann ebenfalls berechnet und im Protokoll ausgegeben werden.
Instrumentierung
Sobald Ihre App in großem Maßstab bereitgestellt ist, ist es wichtig, die Leistung kritischer Vorgänge zu überwachen. Dazu protokollieren wir Instrumentierungen, die an unsere Server gesendet und über alle Benutzer hinweg aggregiert werden. Wir können die Daten dann über die Zeit, nach Plattform, nach Gerätetyp usw. visualisieren.
Über die Brücke gehen
React Native Apps enthalten zwei separate Ausführungsumgebungen – JavaScript und Native. Diese Umgebungen sind relativ unabhängig. Sie laufen jeweils auf separaten Threads und haben Zugriff auf ihre eigenen Daten. Die gesamte Kommunikation zwischen den beiden Umgebungen findet über die React Native „Bridge“ statt. Sie können sich die Bridge als eine bidirektionale Nachrichtenwarteschlange vorstellen. Nachrichten werden in der Reihenfolge verarbeitet, in der sie in jeder der Warteschlangen platziert werden.
Daten werden in serialisierter Form übergeben – UTF16-Text im JSON-Format. Alle E/A-Operationen erfolgen in der nativen Umgebung. Das bedeutet, dass jede Speicher- oder Netzwerkanforderung, die vom JavaScript-Code initiiert wird, über die Bridge gehen muss, und die resultierenden Daten müssen dann serialisiert und in der anderen Richtung über die Bridge zurückgesendet werden. Dies funktioniert gut für kleine Datenmengen, ist aber teuer, sobald die Datengrößen oder die Anzahl der Nachrichten wachsen.
Eine Möglichkeit, diesen Engpass zu mildern, besteht darin, die Übergabe großer Datenmengen über die Bridge zu vermeiden. Wenn keine Verarbeitung in der JavaScript-Umgebung erforderlich ist, belassen Sie sie auf der nativen Seite. Sie kann im JavaScript-Code als „Handle“ dargestellt werden. So handhaben wir alle Bilder, Töne und komplexen Animationsdefinitionen.
Kooperatives Multitasking
JavaScript läuft auf einem einzigen Thread. Wenn der JavaScript-Code Ihrer App über lange Zeiträume ausgeführt wird, blockiert er die Ausführung von Ereignishandlern, Nachrichtenhandlern usw., und die App wird sich nicht reaktionsschnell anfühlen. Wenn Sie eine lang andauernde Operation durchführen müssen, haben Sie mehrere Optionen:
- Implementieren Sie sie als natives Modul und führen Sie sie auf einem separaten Thread aus (nur für React Native anwendbar).
- Teilen Sie die Operation in kleinere Blöcke auf und führen Sie sie als verkettete Aufgaben aus.
- Berechnen Sie nur den Teil des Ergebnisses, der zu diesem Zeitpunkt benötigt wird.
Virtualisierung
Wenn Sie mit langen Datenlisten in einer Benutzeroberfläche arbeiten, ist es wichtig, eine Form der Virtualisierung zu verwenden. Eine virtualisierte Ansicht rendert nur den sichtbaren Inhalt. Während der Benutzer durch die Liste scrollt, werden neu freigegebene Elemente gerendert. Wir haben uns alle verfügbaren virtualisierten Ansichten angesehen, aber keine gefunden, die uns sowohl die Geschwindigkeit als auch die Flexibilität bot, die wir benötigten, daher haben wir unsere eigene Implementierung geschrieben. Unsere VirtualListView durchlief sechs Hauptiterationen, bevor wir uns für ein Design und eine Implementierung entschieden, mit der wir zufrieden waren.
Starten Sie Ihren Start
Die App-Startzeit ist vielleicht die größte Leistungshürde bei React Native Apps. Dies gilt insbesondere für langsamere Android-Geräte. Wir kämpfen weiterhin darum, die Startzeiten auf solchen Geräten zu reduzieren. Hier sind einige Tipps, die wir auf dem Weg gelernt haben.
Verschieben Sie die Modulinitialisierung
In TypeScript- oder JavaScript-Code ist es üblich, am Anfang jedes Moduls eine Reihe von Importanweisungen einzufügen. Hier ist zum Beispiel, was Sie am Anfang der Datei App.tsx im Hello-World-Beispiel finden.
import RX = require('reactxp');
import MainPanel = require('./MainPanel');
import SecondPanel = require('./SecondPanel');
Jeder dieser „require“-Aufrufe initialisiert das angegebene Modul beim ersten Auftreten. Eine Referenz auf dieses Modul wird dann zwischengespeichert, sodass nachfolgende Aufrufe zum „require“ desselben Moduls fast kostenlos sind. Beim Start erfordert das erste Modul mehrere andere Module, von denen jedes mehrere andere Module usw. erfordert. Dies wird fortgesetzt, bis der gesamte Abhängigkeitsbaum des Moduls initialisiert ist. Dies alles geschieht, bevor die erste Zeile Ihres ersten Moduls ausgeführt wird. Mit zunehmender Anzahl von Modulen in Ihrer App steigt die Initialisierungszeit.
Die Lösung dieses Problems ist die verzögerte Initialisierung. Warum die Kosten für die Initialisierung eines Moduls für ein selten verwendetes UI-Panel beim Start bezahlen? Verschieben Sie einfach die Initialisierung, bis es benötigt wird. Dazu verwenden wir ein von Facebook erstelltes Babel-Plugin namens inline-requires. Laden Sie einfach das Skript herunter und erstellen Sie eine Datei mit der Endung „.babelrc“, die etwa so aussieht:
{
"presets": ["react-native"],
"plugins": ["./build/inline-requires.js"]
}
Was macht dieses Skript? Es eliminiert die require-Aufrufe am Anfang Ihrer Module. Immer wenn die importierte Variable in der Datei verwendet wird, fügt es einen Aufruf zu require ein. Das bedeutet, alle Module werden unmittelbar vor ihrer ersten Verwendung initialisiert und nicht beim App-Start. Für große Apps kann dies die Startzeit auf langsamen Geräten um Sekunden verkürzen.
Minifizierung
Für Produktions-Builds ist es wichtig, Ihr JavaScript zu „minifizieren“. Dieser Prozess entfernt unnötige Leerzeichen und kürzt Variablen- und Methodennamen, wo immer möglich. Er reduziert die Größe Ihres JavaScript-Bundles auf der Festplatte und im Speicher und beschleunigt das Parsen Ihres Codes.
Native Modulinitialisierung
React Native enthält eine Reihe integrierter „nativer Module“. Diese bieten Funktionalität, die Sie aus JavaScript aufrufen können. Viele Apps werden nicht alle Standard-nativen Module nutzen. Jedes native Modul kann der App-Initialisierungszeit Dutzende von Millisekunden hinzufügen, daher ist es verschwenderisch, native Module zu initialisieren, die Ihre App nicht verwendet. Unter Android können Sie diesen Overhead eliminieren, indem Sie eine Unterklasse von MainReactPackage erstellen, die für Ihre App spezifisch ist. Kopieren Sie die Methode createViewManagers() in Ihre Unterklasse und kommentieren Sie die nicht verwendeten View-Manager aus. Ändern Sie dann die Methode getPackages() in der ReactInstanceHost-Klasse Ihrer App, um Ihre benutzerdefinierte Klasse anstelle des normalen MainReactPackage zu instanziieren. Diese Technik kann auf langsamen Android-Geräten 100 ms oder mehr von der Startzeit einsparen.
Zusätzliche Ressourcen
Für zusätzliche Tipps zum Performance-Tuning siehe die Seite Performance auf der React Native-Dokumentationsseite von Facebook. Dieser Blog enthält ebenfalls nützliche Tipps.