Krieg auf den Bremsen. Optimieren der Anzahl der Komponenten-Renderings in React Native

Hallo Habr! Mein Name ist Kamo Spertsyan, ich bin bei Profi.ru in der Entwicklung von React Native tätig. Wenn Sie sich für die Verwendung der React Native-Technologie entscheiden, um schnell Produktfunktionen bereitzustellen und sich auf die Entwicklungsgeschwindigkeit zu konzentrieren, treten wahrscheinlich Leistungsprobleme auf. Zumindest ist uns das passiert. Nach sechs Monaten aktiver Entwicklung fiel die Leistung unserer Anwendung unter ein kritisches Niveau - alles war extrem langsam. Daher haben wir die Optimierung aufgenommen - alle „Bremsen“ während des Startvorgangs, Übergänge zwischen Bildschirmen, Rendern von Bildschirmen, Reaktionen auf Benutzeraktionen entfernt. Infolgedessen haben sie in drei Monaten die Benutzererfahrung auf das native Niveau gebracht. In diesem Artikel möchte ich darüber sprechen, wie wir die Anwendung auf React Native optimiert und das Problem des Renderns mehrerer Komponenten gelöst haben.



Ich habe Empfehlungen zusammengestellt, die dazu beitragen, die Anzahl sinnloser Neuzeichnungen von Komponenten zu minimieren. Zur Verdeutlichung vergleiche ich in den Beispielen die "schlechten" und "guten" Implementierungen. Der Artikel ist nützlich für diejenigen, die bereits mit einer schlechten Anwendungsleistung konfrontiert sind, und für diejenigen, die dies in Zukunft nicht zulassen möchten.

Wir verwenden React Native gepaart mit Redux. Einige der Tipps beziehen sich auf diese Bibliothek. Auch im Beispiel verwende ich die Redux-Thunk-Bibliothek, um die Arbeit mit dem Netzwerk zu simulieren.

Wann sollte man über Leistung nachdenken?


In der Tat lohnt es sich, sich von Anfang an an die Arbeit an der Anwendung zu erinnern. Aber wenn Ihre Anwendung bereits langsamer wird - verzweifeln Sie nicht, alles kann repariert werden.

Jeder weiß es, aber für alle Fälle möchte ich erwähnen: Es ist besser, die Leistung auf schwachen Geräten zu überprüfen. Wenn Sie auf leistungsstarken Geräten entwickeln, sind Sie sich der „Bremsen“ der Endbenutzer möglicherweise nicht bewusst. Entscheiden Sie selbst, von welchen Geräten Sie geführt werden. Messen Sie die Zeit oder den FPS in Kontrollkurven, um sie nach der Optimierung mit den Ergebnissen zu vergleichen.

React Native von der Stange bietet die Möglichkeit, FPS-Anwendungen über Entwicklertools → Perf-Monitor anzeigen zu messen. Der Referenzwert beträgt 60 Bilder pro Sekunde. Je niedriger dieser Indikator ist, desto stärker "verlangsamt" sich die Anwendung - reagiert nicht oder reagiert nicht mit einer Verzögerung auf Benutzeraktionen. Einer der Haupteffekte auf FPS ist die Anzahl der Renderings, deren „Schweregrad“ von der Komplexität der Komponenten abhängt.

Beispielbeschreibung


Ich zeige alle Empfehlungen am Beispiel einer einfachen Anwendung mit einer Liste von Nachrichten. Die Anwendung verfügt über einen Bildschirm, auf dem sich FlatListdie Nachrichten befinden. Eine Nachricht ist eine Komponente NewsItem, die aus zwei kleineren Komponenten besteht - der Überschrift ( NewsItemTitle) und dem Text ( NewsItemBody). Das gesamte Beispiel zu sehen ist hier . Weiter im Text finden Sie Links zu verschiedenen Zweigen des Repositorys für spezifische Beispiele. Das Repository wird für Leser verwendet, die Beispiele genauer untersuchen möchten. Der Code im Repository und in den folgenden Beispielen erhebt keinen Anspruch auf Perfektion - er wird ausschließlich zu Demonstrationszwecken benötigt.

Im Folgenden werden alle Komponenten mit Links und Requisiten schematisch dargestellt.


Bei der Rendermethode jeder Komponente habe ich der Konsole die Ausgabe eindeutiger Informationen hinzugefügt:

SCREEN
ITEM_{no}
ITEM_TITLE_{no}
ITEM_BODY_{no}

Wo {no}ist die Seriennummer der Nachrichten, um zwischen verschiedenen Nachrichten-Renderings und mehreren Renderings derselben zu unterscheiden?

Zum Testen jeder refreshNachrichtenliste werden am Anfang zusätzliche Nachrichten hinzugefügt. Gleichzeitig wird in der Konsole folgende Meldung angezeigt:

--------------[ REFRESHING ]--------------

Diese Datensätze helfen zu verstehen, ob es ein Problem in einer bestimmten Komponente gibt, und später festzustellen, ob es möglich war, es zu optimieren.

Bei korrekter Implementierung sollten unser Protokoll nach dem Start und mehrere Updates folgendermaßen aussehen:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
--------------[ REFRESHING ]--------------
SCREEN
ITEM_4
ITEM_TITLE_4
ITEM_BODY_4

Beim ersten Start werden der Bildschirm selbst und zwei erste Nachrichten gezeichnet. Beim Aktualisieren der Karte wird der Bildschirm erneut gerendert, da sich seine Daten wirklich geändert haben. Weitere Neuigkeiten kommen auf. Alle vorherigen Nachrichten werden nicht neu gezeichnet, da sich ihre Daten nicht geändert haben.

Wann wird eine Komponente gerendert?


In React und React Native gibt es zwei Bedingungen zum Rendern einer Komponente:

  1. seine Requisiten / Zustand ändern,
  2. Render der übergeordneten Komponente.

Eine Funktion kann in einer Komponente neu definiert werden shouldComponentUpdate- sie empfängt neue Requisiten und Status als Eingabe und gibt an, ob die Komponente gerendert werden soll. Um unnötige Renderings zu vermeiden, reicht häufig ein flacher Vergleich von Requisiten und Statusobjekten aus. Dies eliminiert beispielsweise unnötige Renderings, wenn sich die übergeordnete Komponente ändert, wenn sie die untergeordnete Komponente nicht beeinflussen. Um nicht jedes Mal manuell einen Oberflächenvergleich zu schreiben, können Sie eine Komponente erben React.PureComponent, die diese Prüfung einschließt.

Wenn wir die Connect Link-Funktion verwenden, erstellt die Redux-Bibliothek eine neue Komponente, die mit dem globalen Status „verbunden“ ist. Änderungen an diesem Status lösen eine Methode ausmapStateToPropsdas gibt neue Requisiten zurück. Als nächstes beginnt ein Vergleich von alten und neuen Requisiten, unabhängig davon, ob die Komponente als deklariert wurde PureComponentoder nicht.

Betrachten Sie diese Nuancen in unserem Beispiel.

Wir NewsItemlassen die Komponente durch connect, NewsItemTitleerben von React.Componentund NewsItemBody- von React.PureComponent.

Vollständiger Beispielcode

export class NewsItemTitle extends React.Component
export class NewsItemBody extends React.PureComponent

So sieht das Protokoll nach einem Board-Update aus:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1

Sie können sehen, dass die Nachrichten- und Überschriftenkomponenten neu gezeichnet werden. Wir werden sie der Reihe nach betrachten.

NewsItemdeklariert mit connect. Als Requisiten erhält diese Komponente eine Kennung, über die sie anschließend Nachrichten erhält in mapStateToProps:

const mapStateToProps = (state, ownProps) => ({
  item: state.newsMap[ownProps.itemKey],
});

Da beim Aktualisieren der Karte alle Nachrichten erneut heruntergeladen werden, wird das Objekt itemaktualisiert und verweist anschließend auf verschiedene Speicherzellen. Mit anderen Worten, es handelt sich um unterschiedliche Objekte, auch wenn alle enthaltenen Felder gleich sind. Daher wird ein Vergleich der vorherigen und der neuen State'ov-Komponente zurückgegeben false. Die Komponente wird neu gerendert, obwohl sich die Daten nicht geändert haben.

NewsItemTitlewird von geerbt React.Componentund wird daher jedes Mal neu gerendert, wenn die übergeordnete Komponente gerendert wird. Dies geschieht unabhängig von den Werten alter und neuer Requisiten.

NewsItemBodygeerbt von React.PureComponent, so vergleicht es alte und neue Requisiten. In den Nachrichten 1 und 2 sind ihre Werte äquivalent, daher wird die Komponente nur für Nachrichten 3 gerendert.

Um die Renderings zu optimierenNewsItemTitledeklariere es einfach als React.PureComponent. Im Fall von müssen Sie NewsItemdie Funktion neu definieren shouldComponentUpdate:

shouldComponentUpdate(nextProps) {
  return !shallowEqual(this.props.item, nextProps.item);
}

Vollständiger Beispielcode

Hier shallowEqualist eine Funktion zum Oberflächenvergleich von Objekten, die Redux bereitstellt. Sie können so schreiben:

shouldComponentUpdate(nextProps) {
  return (
    this.props.item.title !== nextProps.item.title ||
    this.props.item.body !== nextProps.item.body
  );
}

So wird unser Protokoll danach aussehen:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3

Hinweis
shouldComponentUpdate NewsItem , NewsItemTitle . . NewsItemTitle - NewsItem, .

React.memo und Funktionskomponenten


Es ist shouldComponentUpdatenicht möglich, eine Funktionskomponente zu überschreiben . Dies bedeutet jedoch nicht, dass Sie eine Funktionskomponente zur Optimierung in eine Klasse 1 umschreiben müssen. In solchen Fällen wird die Memo- Funktion React.memo bereitgestellt . Es akzeptiert eine Komponenteneingabe und eine optionale Vergleichsfunktion areEqual. Wenn es aufgerufen wird, areEqualerhält es alte und neue Requisiten und sollte das Ergebnis des Vergleichs zurückgeben. Der Unterschied zu dem, shouldComponentUpdatewas zurückkehren areEqualsollte, truewenn die Requisiten gleich sind und nicht umgekehrt.

Beispielsweise kann das NewsItemTitleAuswendiglernen folgendermaßen aussehen:

areEqual(prevProps, nextProps) {
  return shallowEqual(prevProps, nextProps);
}
export OptimizedNewsItemTitle = React.memo(NewsItemTitle, areEqual)

Wenn Sie nicht passieren areEqualin React.memo, dann ein oberflächlicher Vergleich der Requisiten werden gemacht, so dass unser Beispiel vereinfacht werden kann:

export OptimizedNewsItemTitle = React.memo(NewsItemTitle)

Lambda funktioniert in Requisiten


Um Komponentenereignisse zu verarbeiten, können Funktionen an die Requisiten übergeben werden. Das auffälligste Beispiel ist die Implementierung onPress. Oft werden dafür anonyme Lambda-Funktionen verwendet. Angenommen, NewsItemBodywir möchten nur die Vorschau anzeigen, und wenn Sie darauf klicken, den gesamten Text. Um dies zu tun NewsItem, werden NewsItemBodywir beim Rendern die folgende Requisite übergeben:

<NewsItemBody
  ...
  onPress={() => this.props.expandBody()}
  ...
/>

Hier ist , was das Protokoll wie bei dieser Implementierung aussieht , wenn das Verfahren shouldComponentUpdatewird NewsItemgestrichen:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1

Die Nachrichtentexte 1 und 2 werden gerendert, obwohl sich ihre Daten nicht geändert haben, sondern NewsItemBodysind PureComponent. Dies liegt daran, dass für jeden Render der NewsItemWert von Requisiten onPressneu erstellt wird. Technisch gesehen zeigt es onPressbei jedem Rendern auf einen neuen Bereich im Speicher, sodass ein oberflächlicher Vergleich der Requisiten in NewsItemBodyfalse zurückgibt. Das Problem wird durch den folgenden Eintrag behoben:

<NewsItemBody
  ...
  onPress={this.props.expandBody}
  ...
/>

Log:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1

Vollständiger Beispielcode

Leider kann eine anonyme Funktion keinesfalls immer als Methode oder Klassenfeld für einen solchen Datensatz umgeschrieben werden. Der häufigste Fall ist, wenn innerhalb der Lambda-Funktion die Bereichsvariablen der Funktion verwendet werden, in der sie deklariert ist.

Betrachten Sie diesen Fall in unserem Beispiel. Um von der allgemeinen Liste zum Bildschirm einer Nachricht zu wechseln, fügen wir die Verarbeitung des Klickens auf den Nachrichtentext hinzu. Die renderItemKomponentenmethode FlatListsieht folgendermaßen aus:

const renderItem = ({item}) => (
  <NewsItem
    itemKey={item}
    onBodyPress={() => this.onItemBodyPress(item)}
  />
);

Eine anonyme Funktion onBodyPresskann in einer Klasse nicht deklariert werden, da dann die Variable item, die zum Aufrufen einer bestimmten Nachricht benötigt wird, aus dem Bereich verschwindet .

Die einfachste Lösung für das Problem besteht darin, die Signatur der onBodyPressKomponenten- Requisiten NewsItemso zu ändern , dass der erforderliche Parameter beim Aufruf an die Funktion übergeben wird. In diesem Fall ist dies die Nachrichten-ID.

const renderItem = ({item}) => (
  <NewsItem
    itemKey={item}
    onBodyPress={item => this.onItemBodyPress(item)}
  />
);

In diesem Fall können wir die anonyme Funktion bereits in der Komponentenklassenmethode entfernen.

const renderItem = ({item}) => (
  <NewsItem
    itemKey={item}
    onBodyPress={this.onItemBodyPress}
  />
);

Für eine solche Lösung müssen wir jedoch die Komponente ändern NewsItem.

class NewsItemComponent extends React.Component {
render() {
  ...
  return (
      ...
      <NewsItemBody
        ...
        onPress={() => this.props.onBodyPress(this.props.item)}
        ...
      />
      ...
  );
}

Und wieder kehren wir zum angegebenen Problem zurück - wir übergeben der untergeordneten Komponente für jedes Rendering des übergeordneten Elements eine neue Lambda-Funktion. Erst jetzt sind wir ein Level gesunken. Log:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1

Um dieses Problem im Stammverzeichnis zu beheben, können Sie den useCallback- Hook verwenden . Es ermöglicht das Speichern eines Funktionsaufrufs durch Übergeben eines Arguments. Wenn sich das Argument der Funktion nicht ändert, zeigt das Ergebnis des Aufrufs useCallbackauf denselben Speicherbereich. In unserem Beispiel bedeutet dies , dass , wenn die gleiche Nachricht neu zu zeichnen, die prop onPressKomponente NewsItemBodywird sich nicht ändern. Haken können nur in Funktionskomponenten verwendet werden, daher sieht die Komponente NewsItemwie folgt aus:

function NewsItemComponent(props) {
  ...
  const {itemKey, onBodyPress} = props.item;
  const onPressBody = useCallback(() => onBodyPress(itemKey), [itemKey, onBodyPress]);
  return (
    <View>
      ...
      <NewsItemBody
        ...
        onPress={onPressBody}
        ...
      />
    </View>
  );
}

Und das Protokoll:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1

Vollständiger Beispielcode

Arrays und Objekte


In JavaScript werden Funktionen zusammen mit Arrays als Objekte dargestellt. Daher ist das Beispiel aus dem vorherigen Block ein Sonderfall zum Erstellen eines neuen Objekts in Requisiten. Es ist ziemlich häufig, deshalb habe ich es in einen separaten Absatz eingefügt.

Jede Erstellung neuer Funktionen, Arrays oder Objekte in Requisiten führt zu einem Komponenten-Renderer. Betrachten Sie diese Regel im folgenden Beispiel. Lassen Sie uns einen NewsItemBodykombinierten Stil aus zwei Werten übergeben:

<NewsItemBody
  ...
  style={[styles.body, styles.item]}
  ...
/>

Das Protokoll zeigt erneut die zusätzlichen Komponentenrenderer:

SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1

Um dieses Problem zu lösen, können Sie einen separaten Stil auswählen , der die Deklaration des Arrays kombiniert bodyund itembeispielsweise [styles.body, styles.item]in eine globale Variable verschiebt.

Vollständiger Beispielcode

Array-Reduzierungen


Betrachten Sie eine weitere beliebte Quelle für „Bremsen“, die mit der Verwendung verbunden sind FlatList. Eine klassische Anwendung, die eine lange Liste von Elementen vom Server enthält, implementiert die Paginierung. Das heißt, es wird eine begrenzte Anzahl von Elementen in Form der ersten Seite geladen. Wenn die Liste der aktuellen Elemente endet, wird die nächste Seite geladen und so weiter. Ein Reduzierer der Artikelliste könnte folgendermaßen aussehen:

const newsIdList = (state = [], action) => {
  if (action.type === 'GOT_NEWS') {
    return action.news.map(item => item.key);
  } else if (action.type === 'GOT_OLDER_NEWS') {
    return [...state, ...action.news.map(item => item.key)];
  }
  return state;
};

Wenn jede nächste Seite im Stil der Anwendung geladen wird, wird ein neues Array von Bezeichnern erstellt. Wenn wir dieses Array später FlatListan Requisiten übergeben, sehen die Komponenten-Renderprotokolle folgendermaßen aus:

SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..10>
ITEM_<1..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..20>
ITEM_<1..30>

In diesem Beispiel habe ich einige Änderungen an der Testanwendung vorgenommen.

  • Stellen Sie die Seitengröße auf 10 Nachrichten ein.
  • item NewsItem FlatList-, connect. NewsItem React.Component .
  • .
  • . №1 .

Das Beispiel zeigt, dass beim Laden jeder nächsten Seite alle alten Elemente erneut gerendert werden und die alten Elemente und die Elemente der neuen Seite erneut gerendert werden. Für Mathematikliebhaber: Wenn die Seitengröße gleich ist X, werden beim Laden der ii-ten Seite Xdie Elemente gerendert , anstatt nur neue Elemente zu rendern (i - 1) * X + i * X.

„Ok“, sagen Sie, „ich verstehe, warum alle Elemente nach dem Hinzufügen einer neuen Seite gezeichnet werden: Der Reduzierer hat ein neues Array zurückgegeben, einen neuen Speicherbereich, all das. Aber warum müssen wir die alte Liste rendern, bevor wir neue Elemente hinzufügen? “ "Gute Frage", werde ich Ihnen antworten. Dies ist eine Folge der Arbeit mit dem Zustand der Komponente, VirtualizedListauf deren BasisFlatList. Ich werde nicht auf Details eingehen, da sie auf einen separaten Artikel zurückgreifen. Wen kümmert es, ich rate Ihnen, sich mit der Dokumentation und der Quelle zu befassen .

Wie kann man eine solche Nichtoptimalität loswerden? Wir schreiben den Reduzierer neu, damit er nicht für jede Seite ein neues Array zurückgibt, sondern dem vorhandenen Elemente hinzufügt:

Beachtung! Antipattern!
. , , , PureComponent, . , . . Redux.

const newsIdList = (state = [], action) => {
  if (action.type === 'GOT_NEWS') {
    return action.news.map(item => item.key);
  } else if (action.type === 'GOT_OLDER_NEWS') {
    action.news.forEach(item => state.push(item.key));
    return state;
    // return [...state, ...action.news.map(item => item.key)];
  }
  return state;
};

Danach sieht unser Protokoll folgendermaßen aus:

SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..30>

Wir haben das Rendern alter Elemente vor dem Hinzufügen von Elementen zu einer neuen Seite entfernt, aber alte Elemente werden nach dem Aktualisieren der Liste immer noch gezeichnet . Die Anzahl der Renderings für die nächste Seite ist jetzt gleich i * X. Die Formel ist einfacher geworden, aber wir werden hier nicht aufhören. Wir haben nur Xneue Elemente und wir wollen nur Xneue Renderings. Wir werden die bereits bekannten Tricks verwenden, um Nachrichten-Renderings zu entfernen, bei denen die Requisiten nicht geändert wurden. Zurück verbinden mit NewsItem:

SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<11..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<21..30>

Fein! Jetzt können wir mit uns selbst zufrieden sein. Es gibt keinen Ort zur Optimierung.

Vollständiger Beispielcode

Ein aufmerksamer Leser zeigt an, dass es nach dem Anwenden der Verbindung zum NewsItemProtokoll wie im letzten Beispiel aussieht, unabhängig davon, wie Sie den Reduzierer implementieren. Und es wird richtig sein - wenn die Nachrichtenkomponente ihre Requisiten vor dem Rendern überprüft, spielt es keine Rolle, ob das alte Array vom Reduzierer verwendet wird oder ein neues erstellt. Es werden nur neue Elemente gezeichnet und nur einmal. Um jedoch das alte Array zu ändern , anstatt einen neuen zu erstellen erspart uns unnötigen Renderings der Komponente FlatListdarin verwendet VirtualizedListund unnötige Wiederholungen von Requisiten Äquivalenzprüfungen NewsItem. Dies führt bei einer Vielzahl von Elementen auch zu einer Leistungssteigerung.

Die Verwendung von veränderlichen Arrays und Objekten in Reduzierstücken sollte mit äußerster Vorsicht erfolgen. In diesem Beispiel ist dies gerechtfertigt. Wenn Sie jedoch beispielsweise normal PureComponentsind, werden die Komponenten beim Hinzufügen von Elementen zum veränderlichen Array nicht gerendert. Die Requisiten bleiben in der Tat unverändert, da vor und nach der Aktualisierung des Arrays auf denselben Speicherbereich verweist. Dies kann zu unerwarteten Folgen führen. Kein Wunder, dass das beschriebene Beispiel die Prinzipien von Redux verletzt .

Und etwas anderes...


Wenn Sie Bibliotheken auf Präsentationsebene verwenden, empfehle ich Ihnen, sicherzustellen, dass Sie genau verstehen, wie sie implementiert werden. In unserer Anwendung verwenden wir eine Komponente Swipeableaus der Bibliothek react-native-gesture-handler. Sie können einen Block zusätzlicher Aktionen implementieren, wenn Sie eine Karte aus der Liste ziehen.

Im Code sieht es so aus:

<Swipeable
  ...
  renderRightActions={this.renderRightActions}
  ...
>

Methode renderRightActionsoder renderLeftActionsgibt die Komponente zurück, die nach dem Wischen angezeigt wird. Wir haben die Höhe der Platte während des Komponentenwechsels ermittelt und geändert, um sie an den erforderlichen Inhalt anzupassen. Dies ist ein ressourcenintensiver Prozess. Wenn er jedoch während der Wischanimation auftritt, sieht der Benutzer keine Interferenzen.


Das Problem ist, dass die Komponente Swipeabledie Methode renderRightActionszum Zeitpunkt des Renderns der Hauptkomponente aufruft . Alle Berechnungen und sogar das Rendern der Aktionsleiste, die vor dem Wischen nicht sichtbar sind, erfolgen im Voraus. Alle diese Aktionen werden also für alle Karten in der Liste gleichzeitig ausgeführt. Dies verursachte beim Scrollen des Boards erhebliche „Bremsen“.

Das Problem wurde folgendermaßen gelöst. Wenn das Aktionsfenster zusammen mit der Hauptkomponente und nicht als Ergebnis des Wischens gezeichnet wird, gibt die Methode renderRightActionseine leere in der ViewGröße der Hauptkomponente zurück. Ansonsten zeichnen wir das Panel mit zusätzlichen Aktionen wie zuvor.
Ich gebe dieses Beispiel, weil unterstützende Bibliotheken nicht immer wie erwartet funktionieren. Wenn es sich um Bibliotheken auf Präsentationsebene handelt, sollten Sie sicherstellen, dass keine unnötigen Ressourcen verschwendet werden.

Ergebnisse


Nachdem wir die im Artikel beschriebenen Probleme beseitigt hatten, haben wir die Anwendung auf React Native erheblich beschleunigt. Jetzt ist es schwierig, die Leistung von einer ähnlichen zu unterscheiden, die nativ implementiert wurde. Überschüssiges Rendern verlangsamte sowohl das Laden einzelner Bildschirme als auch die Reaktion auf Benutzeraktionen. Vor allem auf den Listen, auf denen Dutzende von Komponenten gleichzeitig gezeichnet werden, fiel dies auf. Wir haben nicht alles optimiert, aber die Hauptbildschirme der Anwendung werden nicht länger langsamer.

Die Hauptpunkte des Artikels sind nachstehend kurz aufgeführt.

  1. React Native : Props/State- .
  2. , React.PureComponent, , .
  3. , shouldComponentUpdate React.Memo .
  4. - . , (shallow compare). , .
  5. Die Unterstützung von Bibliotheken auf Präsentationsebene kann zu unerwarteter Verschwendung von Ressourcen führen. Es lohnt sich, bei der Anwendung vorsichtig zu sein.

Das ist alles. Ich hoffe, Sie finden die Informationen hilfreich. Ich freue mich über jedes Feedback!

Nützliche Quellen


  1. Grundlegendes zum Rendern in React + Redux
  2. Objekte in JavaScript vergleichen
  3. Verbessern der Leistung in reaktiven Funktionskomponenten mit React.memo ()
  4. Wie Discord mit React Native eine native iOS-Leistung erzielt

All Articles