„Wenn ein Arbeiter seine Arbeit gut machen will, muss er zuerst seine Werkzeuge schärfen.“ – Konfuzius, „Die Gespräche des Konfuzius. Lu Linggong“
Titelseite > Programmierung > Erstellen leistungsstarker Full-Stack-Apps mit React, Node.js und MongoDB: Eine Reise in Skalierbarkeit, Geschwindigkeit und Lösungen

Erstellen leistungsstarker Full-Stack-Apps mit React, Node.js und MongoDB: Eine Reise in Skalierbarkeit, Geschwindigkeit und Lösungen

Veröffentlicht am 02.11.2024
Durchsuche:260

Building High-Performance Full-Stack Apps with React, Node.js & MongoDB: A Journey in Scalability, Speed & Solutions

Sie öffnen Ihre Produktions-App und bemerken, dass sie langsam zum Stillstand kommt. Das Frontend reagiert nicht. Bei Backend-APIs kommt es zu einer Zeitüberschreitung. MongoDB-Abfragen scheinen unbegrenzt zu laufen. Ihr Posteingang ist mit Benutzerbeschwerden überschwemmt. Ihr Team drängt sich zusammen und versucht, die Situation zu klären.

Waren Sie dort? Ja, ich auch.

Ich bin ein leitender Full-Stack-Entwickler und habe genug von Apps, die in Ordnung sind, wenn man sie nur als einzelner Benutzer verwendet oder wenn der Problembereich einfach ist, aber dann unter echtem Datenverkehr einfach verkümmern und zusammenbrechen etwas anspruchsvollere Aufgabe.

Bleiben Sie bei mir und ich werde Ihnen zeigen, wie ich diese Bedenken mithilfe von React, Node.js und MongoDB angegangen bin.

Ich werde Ihnen nicht nur ein weiteres einfaches Tutorial geben, sondern eine Geschichte erzählen. Eine Geschichte darüber, wie man reale Probleme angeht und wie man eine schnelle, hoch skalierbare Anwendung erstellt, die den Test der Zeit und alles, was ihr in den Weg gestellt wird, bestehen kann.

1: Als die Reaktion zum Engpass wurde

Wir hatten gerade bei meinem Arbeitsplatz ein Update für unsere mit React entwickelte Web-App ausgerollt. Wir waren voller Zuversicht und davon überzeugt, dass die Benutzer die neuen Funktionen zu schätzen wissen würden.

Es dauerte jedoch nicht lange, bis wir Beschwerden erhielten: Die App wurde extrem langsam geladen, Übergänge stotterten und die Benutzer waren zunehmend frustriert. Obwohl man wusste, dass die neuen Funktionen vorteilhaft waren, führten sie unbeabsichtigt zu Leistungsproblemen. Unsere Untersuchung ergab ein Problem: Die App bündelte alle ihre Komponenten in einem einzigen Paket, was Benutzer dazu zwang, bei jedem Zugriff auf die App alles herunterzuladen.

Die Lösung: Wir haben ein sehr nützliches Konzept namens Lazy Loading implementiert. Ich war schon einmal auf diese Idee gestoßen, aber sie war genau das, was wir brauchten. Wir haben die Struktur der App komplett überarbeitet und sichergestellt, dass sie nur die notwendigen Komponenten lädt, wenn sie benötigt werden.

Hier ist ein Einblick, wie wir diese Lösung implementiert haben:

const Dashboard = React.lazy(() => import('./Dashboard'));
const Profile = React.lazy(() => import('./Profile'));

Loading...}>
  
  


Das Ergebnis: Die Auswirkungen dieser Änderung waren geradezu bemerkenswert. Wir konnten bei unserem Paket einen satten Rückgang um 30 % feststellen, und die Benutzer erlebten eine viel schnellere anfängliche Ladezeit. Das Beste daran war, dass die Benutzer keine Ahnung hatten, dass bestimmte Teile der App noch geladen wurden. Wir haben Suspense mit Bedacht eingesetzt und eine einfache, nicht aufdringliche Lademeldung angezeigt.

2: Das Biest des Staatsmanagements in React zähmen

Während wir ein paar Monate vorspulen, kam unser Entwicklungsteam auf Hochtouren und lieferte viele neue Funktionen. Aber mit dem Wachstum hatten wir versehentlich begonnen, eine, wie ich es nenne, komplexere App zu entwickeln. Redux wurde schnell eher zu einer Belastung als zu einem Hilfsmittel bei der Erleichterung einfacher Interaktionen.

Also habe ich einige Zeit damit verbracht, einen POC für eine bessere Alternative zu erstellen. Ich habe die ganze Sache dokumentiert und mehrere Treffen zum Wissensaustausch darüber geleitet, wie dieser Ansatz möglicherweise aussehen könnte. Wir haben uns schließlich als Gruppe entschieden, React Hooks (und insbesondere useReducer) als unsere vorgeschlagene Lösung für die Statusverwaltung auszuprobieren, weil wir letztendlich einfacheren Code und weniger von dem massiven Laufzeitbedarf wollten, den neuere Versionen von Redux mit einem wachsenden Overhead mit vielen kleineren eigenständigen Versionen hatten Zustände.

Die darauf folgende Transformation war geradezu revolutionär. Wir haben Dutzende Zeilen Standardcode durch eine prägnante, leicht verständliche Hook-Logik ersetzt. Hier ist ein anschauliches Beispiel dafür, wie wir diesen neuen Ansatz umgesetzt haben:

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count   1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

const CounterContext = React.createContext();

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    
      {children}
    
  );
}

Das Ergebnis: Die Auswirkungen dieses Übergangs waren tiefgreifend und weitreichend. Unsere Anwendung wurde deutlich vorhersehbarer und leichter zu überdenken. Die jetzt schlankere und intuitivere Codebasis ermöglichte es unserem Team, viel schneller zu iterieren. Am wichtigsten ist vielleicht, dass unsere Nachwuchsentwickler von einer deutlichen Verbesserung ihrer Fähigkeit berichteten, in der Codebasis zu navigieren und sie zu verstehen. Das Endergebnis war eine Win-Win-Situation: weniger zu wartender Code, weniger zu beseitigende Fehler und ein deutlich zufriedeneres und produktiveres Entwicklungsteam.

3: Das Backend-Schlachtfeld erobern – Node.js-APIs für Spitzenleistung optimieren

Während wir viele Verbesserungen an unserem Frontend vornehmen konnten, hatten wir bald darauf mehrere Probleme im Backend. Unsere API-Leistung wurde schrecklich und es gab insbesondere einige Endpunkte, die anfingen, miserabel zu funktionieren. Diese Endpunkte tätigen eine Reihe von Aufrufen an verschiedene Dienste von Drittanbietern und mit der wachsenden Benutzerbasis war das System nicht in der Lage, diese Last zu bewältigen.

Es war völlig normal, was falsch war: Wir waren NICHT parallel! d. h. Anfragen an jeden Endpunkt wurden sequentiell bearbeitet, d. h. jeder nächste Anruf würde darauf warten, dass der vorherige Anruf abgeschlossen wurde. In diesem umfangreichen System (hunderttausend Anfragen) erwies es sich als katastrophal.

Die Lösung: Um dieses Problem zu beheben, haben wir beschlossen, einen Großteil unseres Codes neu zu schreiben und die Leistungsfähigkeit von Promise.all() zu nutzen, um die API-Anfrage gleichzeitig zu stellen. Das bedeutet, dass Sie mehrere Anfragen starten und nicht warten müssen, bis jeder Anruf beendet ist, um den nächsten zu starten.

Um dies zu erreichen, starten wir keinen API-Aufruf, warten nicht, bis er abgeschlossen ist, erstellen einen weiteren und so weiter…

Anstatt einfach Promise.all() zu verwenden, wurde alles auf einmal und viel schneller gestartet.

Hier ist ein Einblick, wie wir diese Lösung implementiert haben:

const getUserData = async () => {
  const [profile, posts, comments] = await Promise.all([
    fetch('/api/profile'),
    fetch('/api/posts'),
    fetch('/api/comments')
  ]);
  return { profile, posts, comments };
};

Das Ergebnis: Die Auswirkungen dieser Optimierung waren unmittelbar und erheblich. Wir konnten eine bemerkenswerte Verkürzung der Reaktionszeiten um 50 % beobachten und unser Backend zeigte eine deutlich verbesserte Widerstandsfähigkeit bei hoher Belastung. Die Benutzer erlebten keine frustrierenden Verzögerungen mehr und wir konnten einen dramatischen Rückgang der Anzahl der Server-Timeouts feststellen. Diese Verbesserung verbesserte nicht nur das Benutzererlebnis, sondern ermöglichte es unserem System auch, ein viel höheres Anfragevolumen ohne Leistungseinbußen zu verarbeiten.

4: Die MongoDB-Quest – Das Datenmonster zähmen

Als unsere Anwendung an Zugkraft gewann und unsere Benutzerbasis um Größenordnungen wuchs, mussten wir uns einem neuen Hindernis stellen: Wie skaliert man die Daten? Unsere einst reaktionsfähige MongoDB-Instanz geriet ins Stocken, als sie Millionen von Dokumenten verarbeiten musste. Abfragen, die früher in Millisekunden ausgeführt wurden, dauerten Sekunden – oder es kam zu einer Zeitüberschreitung.

Wir haben uns ein paar Tage lang mit den Leistungsanalysetools von MongoDB befasst und den großen Bösewicht identifiziert: nicht indizierte Abfragen. Einige unserer häufigsten Anfragen (z. B. Anfragen nach Benutzerprofilen) bestanden darin, ganze Sammlungen zu scannen, für die sie grundsolide Indizes verwenden konnten.

Die Lösung: Aufgrund der Informationen, die uns zur Verfügung standen, wussten wir, dass wir nur zusammengesetzte Indizes für die am häufigsten nachgefragten Felder erstellen mussten und dass dies die Suchzeit für den Datenbankkörper endgültig verkürzen würde. So haben wir es gemacht, als es um die Felder „Benutzername“ und „E-Mail“ ging.

db.users.createIndex({ "username": 1, "email": 1 });

Das Ergebnis: Die Auswirkungen dieser Optimierung waren geradezu bemerkenswert. Abfragen, deren Ausführung zuvor bis zu 2 Sekunden gedauert hatte, wurden jetzt in weniger als 200 Millisekunden abgeschlossen – eine zehnfache Leistungssteigerung. Unsere Datenbank hat ihre schnelle Reaktionsfähigkeit wiedererlangt, sodass wir ein deutlich höheres Datenverkehrsvolumen ohne spürbare Verlangsamung bewältigen können.

Dabei haben wir jedoch noch nicht aufgehört. Da wir erkannten, dass sich unser rasanter Wachstumskurs voraussichtlich fortsetzen würde, ergriffen wir proaktive Maßnahmen, um die langfristige Skalierbarkeit sicherzustellen. Wir haben Sharding implementiert, um unsere Daten auf mehrere Server zu verteilen. Diese strategische Entscheidung ermöglichte uns eine horizontale Skalierung und stellte sicher, dass unsere Fähigkeit, Daten zu verarbeiten, parallel zu unserer wachsenden Benutzerbasis wuchs.

5. Microservices nutzen – Das Skalierbarkeitsrätsel lösen

Da sich unsere Benutzerbasis weiter vervielfachte, wurde es immer offensichtlicher, dass wir nicht nur unsere Infrastruktur skalieren, sondern auch unsere Anwendung weiterentwickeln mussten, um sicher skalieren zu können. Die monolithische Architektur passte gut zu uns, als wir ein kleineres Team waren, aber mit der Zeit wurde sie ziemlich umständlich. Wir wussten, dass wir den Sprung wagen und mit dem Aufbau einer Microservices-Architektur beginnen mussten – eine einschüchternde Aufgabe für jedes Engineering-Team, aber eine mit großen Vorteilen in Bezug auf Skalierbarkeit und Zuverlässigkeit.

Eines der größten Probleme war die Kommunikation zwischen den Diensten. HTTP-Anfragen funktionieren in unserem Fall wirklich nicht und haben zu einem weiteren Engpass im System geführt, da eine große Anzahl von Vorgängen rastlos auf eine Antwort wartete und bei Bedarf Programme abbrach, man hatte zu viel zu tun. An diesem Punkt wurde uns klar, dass die Verwendung von RabbitMQ hier eine naheliegende Lösung ist, also haben wir es angewendet, ohne groß darüber nachzudenken.

Hier ist ein Einblick, wie wir diese Lösung implementiert haben:

const amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', (err, conn) => {
  conn.createChannel((err, ch) => {
    const queue = 'task_queue';
    const msg = 'Hello World';

    ch.assertQueue(queue, { durable: true });
    ch.sendToQueue(queue, Buffer.from(msg), { persistent: true });
    console.log(`Sent ${msg}`);
  });
});

Das Ergebnis: Der Übergang selbst und die Kommunikation über RabbitMQ sahen aus unserer Sicht wie Magie aus … und die Zahlen bestätigten es!!! Wir wurden glückliche Besitzer von lose gekoppelten Mikrodiensten, bei denen jeder Dienst für sich skaliert werden konnte. Plötzlich waren echte Verkehrsspitzen in der konkreten DNS-Zone nicht mehr mit der Angst verbunden, dass das System ausfällt (denn egal, welcher Dienstbetrieb die gleichen Fragen stellt, weil sie immer kaskadiert sind), sondern funktionierten gut, da die übrigen Teile/Betriebe einfach ruhig die Hand hoben und sagten: „ Ich kann schlafen, meine Liebe. Auch die Wartung wurde einfacher und weniger problematisch, während das Hinzufügen neuer Funktionen oder Updates schneller und sicherer im Betrieb war.

Fazit: Weichen für zukünftige Innovationen stellen

Jeder Schritt auf dieser aufregenden Reise war eine Lektion und erinnerte uns daran, dass Full-Stack-Entwicklung mehr ist als das Schreiben von Code. Es geht darum, komplizierte, miteinander verbundene Probleme zu verstehen und dann zu lösen – von der Beschleunigung unserer Frontends und dem Aufbau von Backends, die Fehlern standhalten, bis hin zum Umgang mit Datenbanken, die skalieren, während Ihre Benutzerbasis explodiert.

Mit Blick auf die zweite Hälfte des Jahres 2024 und darüber hinaus wird sich die steigende Nachfrage nach Webanwendungen nicht verlangsamen. Wenn wir uns weiterhin auf die Entwicklung skalierbarer, leistungsoptimierter und gut strukturierter Anwendungen konzentrieren, sind wir heute in der Lage, jedes Problem zu lösen – und die anderen Herausforderungen unserer Zukunft zu meistern. Diese Erfahrungen aus dem wirklichen Leben haben meine Herangehensweise an die Full-Stack-Entwicklung stark beeinflusst – und ich kann es kaum erwarten zu sehen, wohin diese Einflüsse unsere Branche weiterhin vorantreiben werden!

Aber wie wäre es mit dir? Sind Sie mit ähnlichen Hindernissen konfrontiert oder hatten Sie Glück mit anderen kreativen Möglichkeiten, diese Probleme zu überwinden? Ich würde gerne Ihre Geschichten oder Erkenntnisse hören – lassen Sie es mich in den Kommentaren wissen oder verbinden Sie sich mit mir!

Freigabeerklärung Dieser Artikel wird unter: https://dev.to/mukhilpadmanabhan/building-high-performance-full-stack-apps-react-nodejs-mongodb-aourne-journey-in-scalierbarkeitspeed-solutions-3fk0?1 reproduziert, wenn es ein Verletzung gibt.
Neuestes Tutorial Mehr>

Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.

Copyright© 2022 湘ICP备2022001581号-3