Es gibt viele Mythen über die Leistung von Electron-Apps. Mit NAPI-RS, das Rust mit Node.js mischt, haben wir es von 800 ms auf 75 ms (einzelne Datei) reduziert. Erfahren Sie, wie das geht.
A QUICK SUMMARY – FOR THE BUSY ONES
TABLE OF CONTENTS
Electron hat einen großen Vorteil: Es ermöglicht Entwicklern, Desktop-Anwendungen für verschiedene Systeme mit derselben Codebasis zu erstellen. Das ist sicher.
Jedoch Wir hören oft, dass es langsam ist, viel Speicher verbraucht und mehrere Prozesse hervorruft, die das gesamte System verlangsamen.
Trotzdem werden einige sehr beliebte Anwendungen mit Electron erstellt, darunter:
Nicht alle von ihnen sind perfekt, aber Visual Studio Code scheint das Gegenteil zu sein.
Ist Electrons Leistung dann so schlecht? Unserer Erfahrung nach ist Electron sogar ziemlich leistungsstark und reaktionsschnell. Es sind nur ein paar Anpassungen erforderlich.
In diesem Artikel zeigen wir Ihnen wie wir Engpässe in unserer Electron-Anwendung reduziert und sie schnell gemacht haben.
Die vorgestellte Methode kann auf Node.js-basierte Anwendungen wie API-Server oder andere Tools angewendet werden, die eine hohe Leistung erfordern.
Wir werden uns einen Electron-basierten Game Launcher ansehen.
Wenn Sie Spiele spielen, haben Sie wahrscheinlich einige davon installiert. Die meisten Launcher laden Spieldateien herunter, installieren Updates und verifizieren Dateien, sodass Spiele problemlos gestartet werden können.
Es gibt Teile, die wir nicht beschleunigen können und die abhängig sind, z. B. von der Verbindungsgeschwindigkeit, aber wenn es darum geht, heruntergeladene oder gepatchte Dateien zu überprüfen, ist das eine andere Geschichte. Und Wenn das Spiel groß ist, kann der gesamte Vorgang eine beeindruckende Zeit in Anspruch nehmen.
Das ist unser Fall.
Die App, die wir analysieren werden, ist verantwortlich für das Herunterladen von Dateien und, falls berechtigt, das Anwenden von Binär-Patches. Wenn das erledigt ist, müssen wir sicherstellen, dass nichts beschädigt wird. Es spielt keine Rolle, was die Korruption verursacht, unsere Nutzer wollen das Spiel spielen, und wir müssen es möglich machen.
Lassen Sie mich Ihnen jetzt ein paar Zahlen geben. Unsere Spiele bestehen aus 44 Dateien mit einer Gesamtgröße von ca. 4,7 GB.
Wir müssen sie alle überprüfen, nachdem wir das Spiel oder ein Update heruntergeladen haben. Wir haben benutzt https://www.npmjs.com/package/crc um den CRC jeder Datei zu berechnen und ihn mit der Manifestdatei zu vergleichen. Schauen wir uns an, wie performant dieser Ansatz ist, Zeit für einige Benchmarks.
Alle Benchmarks werden auf einem ausgeführt 2021 MacBook Pro 14 Zoll M1 Pro.
Zunächst benötigen wir einige Dateien zur Überprüfung. Wir können ein paar mit dem Befehl erstellen
mkfile -n 200m test_200m_1
Aber wenn wir uns den Inhalt ansehen, werden wir sehen, dass alles Nullen sind!
Das könnte zu verzerrten Ergebnissen führen. Stattdessen verwenden wir diesen Befehl:
füge if=/dev/urandom of=test_200m_1 bs=1M count=200
Lassen Sie uns 10 Dateien mit jeweils 200 MB erstellen. Da die darin enthaltenen Daten zufällig sind, sollten sie unterschiedliche Prüfsummen haben.
Der Benchmarkcode:
Es dauert etwa 800 ms, um den Lesestream zu erstellen und die Prüfsumme schrittweise zu berechnen. Wir bevorzugen Streams, weil wir es uns nicht leisten können, große Dateien in den Systemspeicher zu laden. Wenn wir CRC32 für alle Dateien nacheinander berechnen, ist das Ergebnis ~16700 ms. Es verlangsamt sich nach der 3. Datei.
Ist es besser, wenn wir verwenden Versprochen. Alles um sie gleichzeitig auszuführen? Nun... das ist an der Grenze des Messfehlers. Es variiert bei etwa ~16100 ms.
Also, hier sind unsere bisherigen Ergebnisse:
Es gibt viele Wege, die Sie bei der Optimierung einer Electron-App einschlagen können, aber wir sind hauptsächlich an folgenden Themen interessiert:
Worker Thread benötigt einen gewissen Boilerplate-Code. Außerdem kann es problematisch sein, wenn Ihre Codebasis in TypeScript ist. Es ist machbar, erfordert jedoch zusätzliche Tools wie ts-node oder configuration. Wir wollen nicht, wer weiß wie viele Worker-Threads erzeugen — das wäre auch ineffizient. Das Leistungsproblem liegt woanders. Es wird langsam sein, wo auch immer wir diese Berechnung durchführen.
Schlußfolgerung: Das Spawnen von Worker-Threads verlangsamt unsere App noch mehr, daher ist NodeJS Worker Threads nichts für uns.
Wenn wir es schnell wollen, sieht die Node-API nach einer perfekten Lösung aus. Eine in C/C++ geschriebene Bibliothek muss schnell sein. Wenn Sie lieber C++ als C verwenden, Node-Addon-API kann helfen. Dies ist wahrscheinlich eine der besten verfügbaren Lösungen, zumal sie offiziell vom Node.js Team unterstützt wird. Es ist super stabil, wenn es einmal gebaut ist, aber es kann während der Entwicklung schmerzhaft sein. Fehler sind oft alles andere als leicht zu verstehen. Wenn Sie also kein Experte in C sind, kann es Ihnen sehr leicht in den Hintern treten.
Schlußfolgerung: Wir haben keine C-Kenntnisse, um die Fehler zu beheben, also ist Node-API nichts für uns.
Jetzt wird es interessant: Neon Bindings. Rust in Node.js klingt toll, ein weiteres Buzzword, aber ist es nur ein Buzzword? Laut Neon wird es von beliebten Apps wie 1Password und Signal verwendet https://neon-bindings.com/docs/example-projects, aber schauen wir uns die andere RUST-basierte Option an, nämlich NAPI-RS.
Schlußfolgerung: Neon Bindings sieht vielversprechend aus, aber lassen Sie uns sehen, wie es im Vergleich zu unserer letzten Option abschneidet.
Wenn wir uns die Dokumentation ansehen, sehen die Dokumente von NAPI-RS viel besser aus als die von Neon. Das Framework wird von einigen großen Namen der Branche gesponsert. Die umfangreiche Dokumentation und der Support großer Marken sind genug Gründe für uns, uns für NAPI-RS und nicht für Neon Bindings zu entscheiden.
Schlußfolgerung: NAPI-RS bietet eine bessere Dokumentation als vergleichbare Neon-Bindungen und ist daher eine sicherere Wahl.
Um unsere Electron-App zu optimieren, verwenden wir NAPI-RS, das Rust mit Node.js mischt.
Rust ist aufgrund seiner Leistung, Speichersicherheit, Community und Tools (Cargo, Rust-Analyzer) eine attraktive Ergänzung zu Node.js. Kein Wunder, dass es eine der beliebtesten Sprachen ist und immer mehr Unternehmen ihre Module auf Rust umschreiben.
Mit NAPI-RS müssen wir eine Bibliothek erstellen, die Folgendes beinhaltet https://crates.io/crates/crc32fast um CRC32 extrem schnell zu berechnen. NAPI-RS bietet uns großartige, sofort einsatzbereite Workflows zum Erstellen von NPM-Paketen, sodass das Erstellen und Integrieren in das Projekt ein Kinderspiel ist. Prebuilts werden unterstütztauch, damit Sie die Rust-Umgebung überhaupt nicht benötigen, um sie zu verwenden, wird der richtige Build heruntergeladen und verwendet. Egal, ob Sie Windows, Linux oder macOS verwenden (Apple M1-Maschinen sind ebenfalls auf der Liste.)
Mit der crc32fast-Bibliothek werden wir Verwenden Sie die Hasher-Instanz, um die Prüfsumme aus dem Lesestream zu aktualisieren, wie in der JS-Implementierung:
Es klingt vielleicht wie ein falsches oder ungültiges Ergebnis, aber es sind nur 75 ms für eine einzelne Datei! Es ist zehnmal schneller als die JS-Implementierung. Wenn wir alle Dateien nacheinander verarbeiten, sind es ungefähr 730 ms, also es skaliert auch viel besser.
Aber das ist noch nicht alles. Es gibt noch eine ganz einfache Optimierung, die wir vornehmen können. Anstatt die native Bibliothek N-mal aufzurufen (wobei N die Anzahl der Dateien ist), können wir dafür sorgen, dass sie ein Array von Pfaden akzeptiert und für jede Datei einen Thread erzeugt.
Denken Sie daran: Rust hat keine Begrenzung für die Anzahl der Threads, da Dies sind vom System verwaltete Betriebssystem-Threads. Das hängt vom System ab. Wenn du also weißt, wie viele Threads erzeugt werden und es nicht sehr hoch ist, solltest du auf Nummer sicher gehen. Andernfalls würden wir empfehlen, ein Limit festzulegen und Dateien zu verarbeiten oder die Berechnung in Blöcken durchzuführen.
Stellen wir unsere Berechnung in einen Thread pro einzelne Datei und geben alle Prüfsummen auf einmal zurück:
Wie lange dauert es, die native Funktion mit einer Reihe von Pfaden aufzurufen und alle Berechnungen durchzuführen?
Nur 150 ms, ja, es ist SO schnell. Um 100% sicher zu gehen, haben wir unser MacBook neu gestartet und zwei weitere Tests durchgeführt.
Lassen Sie uns alle Ergebnisse zusammenfassen und sehen, wie sie sich vergleichen.
Es ist erwähnenswert, dass das Aufrufen der nativen Funktion mit einem leeren Array 124584 Nanosekunden dauert, was 0,12 ms entspricht, sodass der Overhead sehr gering ist.
Wie eingangs erwähnt, gilt all dies für Web-APIs, CLI-Tools und Electron. Im Grunde genommen für alles, wo Node.js verwendet wird.
Aber bei Electron gibt es noch eine Sache, an die man sich erinnern sollte. Electron bündelt die App in einem Archiv namens app.asar. Einige Node-Module müssen entpackt werden, um von der Runtime geladen zu werden. Die meisten Bundler wie Electron Builder oder Forge speichern diese Module automatisch außerhalb der Archivdatei, aber es kann vorkommen, dass unsere Bibliothek in der Asar-Datei verbleibt. Wenn ja, sollten Sie angeben, welche Bibliotheken entpackt bleiben sollen. Es ist nicht verpflichtend, aber reduziert den Aufwand beim Entpacken und Laden dieser .node-Dateien.
Wie Sie sehen, gibt es mehrere Möglichkeiten, Teile Ihrer Electron-Anwendung zu beschleunigen, insbesondere wenn es um umfangreiche Berechnungen geht. Zum Glück können Entwickler aus verschiedenen Sprachen und Strategien wählen, um ein breites Spektrum an Anwendungsfällen abzudecken.
In unserer App ist das Überprüfen von Dateien nur ein Teil des gesamten Launcher-Prozesses. Der langsamste Teil für die meisten Spieler ist das Herunterladen der Dateien. Dies kann jedoch nicht über das hinaus optimiert werden, was Ihr Internetdienstanbieter anbietet. Außerdem haben einige Player ältere Computer mit HDD-Festplatten, bei denen I/O und nicht die CPU der Engpass sein könnten.
Aber wenn es etwas gibt, das wir zu vernünftigen Kosten verbessern und leistungsfähiger machen können, sollten wir uns darum bemühen. Wenn es in Ihrer Anwendung Funktionen oder Module gibt, die entweder in Rust oder C neu geschrieben werden können, warum nicht experimentieren? Solche Optimierungen könnten die Gesamtleistung Ihrer App erheblich verbessern.
Our promise
Every year, Brainhub helps 750,000+ founders, leaders and software engineers make smart tech decisions. We earn that trust by openly sharing our insights based on practical software engineering experience.
Authors
Read next
Popular this month