Tree Shaking React Native Apps
Wie wir Optimierungstechniken, die für Web-Apps üblich sind, auf unsere React Native-App angewendet haben, um die Startzeiten um 20 % zu verkürzen.
Baum was?
Zugegeben, Tree Shaking ist vielleicht ein verwirrender Begriff. Vielleicht haben Sie schon einmal davon als „Import Elision“ in TypeScript gehört. Tree Shaking ist eine Form der Eliminierung von totem Code, die sich speziell auf das Entfernen ungenutzter Exporte bezieht. Wenn wir alle Module verketten würden, sind unbenutzte Exporte effektiv toter Code und können entfernt werden. Allerdings ist der Prozess der Ermittlung ungenutzter Exporte nicht trivial. Tree Shaking wird normalerweise auf der Compiler-/Bündlerebene (z. B. Webpack oder ESBuild ) implementiert, nicht durch eine JavaScript-Engine (z. B. V8 oder Hermes). Viele Muster in JavaScript können Tree Shaking brechen, aber in diesem Artikel möchte ich mich auf einen Aspekt konzentrieren: das Modulsystem. Die beiden relevanten Modulsysteme, die wir hier verstehen müssen, sind CommonJS-Module undES-Module .
CommonJS wird verwendet, wenn Sie module.exports = {}oder schreiben exports.someMethod = () => {}. importES - Module werden durch eine exportSyntax identifiziert. Für Compiler ist es schwieriger, Tree Shaking mit CommonJS als mit ES-Modulen auf Code anzuwenden. CommonJS-Module sind oft dynamisch, während ES-Module statisch analysiert werden können. Beispielsweise ist es nicht trivial, alle Exportkennungen im folgenden Code statisch zu erkennen:
Da ES-Module per Design statisch analysierbar sind, haben Compiler eine einfachere Zeit, ungenutzte Exporte zu erkennen.
Es ist daher in Ihrem besten Interesse, Ihrem optimierenden Compiler ES-Module zu geben, mit denen er arbeiten kann, und keine CommonJS-Module.
Hintergrund
Bevor ich zu Klarna kam, hatte ich keine Erfahrung mit der Arbeit mit React Native. Während eines routinemäßigen Refactorings habe ich das folgende Diff angewendet:
Unter der Annahme, dass der verwendete Bundler someFeatureMethodals unbenutzt gelten würde, wenn SOME_STATIC_FLAGer falsch ist, und somit some-feature-moduleaus dem endgültigen Bundle entfernt wird. Während der Codeüberprüfung wurde dieser Unterschied als problematisch markiert, also setzte ich mich hin und überprüfte meine Annahmen und wo diese gebrochen waren. Glücklicherweise sind wir bereits ein paar Monate zuvor auf Webpack (in Form von Re.Pack ) umgestiegen, um Bundle-Splitting mitReact.lazy zu ermöglichen . Dadurch ist es für mich einfacher, den Bündelungsprozess so zu konfigurieren, dass ich das endgültige JavaScript-Bundle überprüfen kann. Nur Hermes musste in unserem Fall deaktiviert werden, um die endgültige JavaScript-Ausgabe anzuzeigen.
Nach einigem Ausprobieren, um es einfacher zu finden, wo some-feature-moduleimportiert wird, habe ich die folgende Zeile entdeckt: c=(n(463526),n(456189)Der Kommaoperator ist etwas, das Sie normalerweise nicht verwenden, also lassen Sie mich zusammenfassen, was er tut: Er wertet alle Operanden aus und verwendet nur den Rückgabewert von der letzte Operand. Mit anderen Worten, der Rückgabewert von n(463526)wurde nicht verwendet. Da ich bereits Erfahrung im Arbeiten mit Tree-Shaking im Web hatte, war mir ziemlich klar, was das vor der Minifizierung war: require('some-feature-module')(Webpack wandelt die importierten Quellstrings in Zahlen um).
Webpack hat tatsächlich erkannt, dass someFeatureMethodes nicht verwendet wurde, und daher seine Verwendung entfernt. Webpack verzichtete jedoch darauf, ungenutzte Exporte aus dem Modul zu entfernen, und behielt daher den Import bei, da es nicht wusste, ob das Modul Nebenwirkungen hatte. Wenn ein Modul Nebeneffekte hat, können wir es nicht einfach aus dem Paket entfernen, da dies den Ablauf des Programms verändern würde.
Alles, was wir tun mussten, damit das ursprüngliche Diff wie erwartet funktioniert, war sicherzustellen, dass Tree Shaking auf das endgültige Bundle angewendet wird.
Implementierung
Es kommt darauf an, sicherzustellen, dass Sie ES-Module nicht nach CommonJS transpilieren, bevor Webpack alle Module bündelt. Wenn Sie die Metro Babel-Voreinstellung verwenden (Standard für neue React Native-Apps), besteht die meiste Arbeit darin, Folgendes zu aktivieren disableImportExportTransform:
Beachten Sie, dass diese Option derzeit nicht dokumentiert ist und jederzeit entfernt werden kann.
Wir mussten Webpack auch anweisen, Einstiegspunkte zu verwenden, die ES-Module anstelle von CommonJS-Modulen verwenden. Für einzelne Dateien bedeutet dies, dass wir bevorzugen, .mjswährend wir für Pakete Webpack anweisen mussten, das moduleHauptfeld zu verwenden .
Dies offenbarte jedoch Probleme damit, wie wir JavaScript geschrieben haben und wie Code im React Native-Ökosystem geschrieben wird. Wir haben 3 Klassen von Problemen identifiziert.
Exportieren unterschiedlicher Syntax in mainundmodule
Diese Hauptfelder sollten nur zur Unterscheidung des Modulsystems ( mainfür CommonJS, modulefür ES-Module) verwendet werden. Viele Pakete liefern jedoch eine modernere Syntax vom moduleEinstiegspunkt aus. Beispielsweise wird die classSyntax derzeit nicht von Hermes unterstützt .
Im Moment transpilieren wir alle node_modulesInhalte auf die ES5-Syntax bzw. Syntax, die von Hermes unterstützt wird, indem rulewir der Webpack-Konfiguration eine benutzerdefinierte Funktion hinzufügen:
Importieren eines CommonJS-Moduls mit mehrdeutiger Syntax
Webpack kann keine Exporte von Modulen finden, die Modulsysteme mischen. React Native selbst liefert seine Quelldateien jedoch mit einem gemischten Modulsystem aus, z
Die Lösung hier besteht darin, diese Module weiterhin in CommonJS zu transpilieren (wodurch Tree Shaking deaktiviert wird), indem ein Special rulezur Webpack-Konfiguration hinzugefügt wird:
Import-IDs nicht vorhanden
Dies ist eigentlich ein SyntaxError in JavaScript, den die meisten nicht kennen. Zum Beispiel import { doesNotExist } from 'some-module';wird eine geworfen SyntaxError. Es ist größtenteils ein Ärgernis für Entwickler, kann aber zu tatsächlichen Laufzeitproblemen führen. Wir haben diese strenge Implementierung von ES-Modulen in Webpack erzwungen, indem wir sie module.parser.javascript.exportsPresencein der Webpack-Konfiguration aktiviert haben.
Die meisten dieser Probleme wurden durch das erneute Exportieren von Typen in TypeScript verursacht, z
Glücklicherweise kann TypeScript diese Probleme auf Typebene kennzeichnen, indem es Folgendes aktiviert isolatedModules:
typeModifikatoren für Importnamen sind neu in TypeScript 4.5. Das Hinzufügen von Unterstützung für typeModifikatoren für Importnamen war eine ziemliche Herausforderung, da wir den von uns verwendeten ESLint-Parser , Prettier und TypeScript aktualisieren mussten.
Das Hinzufügen typevon Modifikatoren zu Importnamen führt dazu, dass Babel Typimporte entfernt, die zur Laufzeit nicht existieren.
Ergebnisse
Die anfängliche Implementierung war ziemlich hacky. Die allerersten Ergebnisse zeigten jedoch bereits eine mittlere Startverbesserung von 20 % auf beiden Plattformen (Samsung Galaxy S9 2,2 s von 2,8 s und iPhone 11 640 ms von 802 ms).
Was wir sahen, war eine 46-prozentige Reduzierung unseres anfänglichen, kritischen JavaScript-Blocks. Die Gesamtgröße des ausgelieferten JavaScripts ging um 14 % zurück. Der Unterschied wird größtenteils dem Verschieben von Code vom Haupt-Chunk in asynchrone Chunks (Features und Routen) zugeschrieben.
Diese Bilder wurden von der erstaunlichen statoscope.tech erstellt , die uns bei der Analyse dieser Änderung geholfen hat und uns weiterhin dabei helfen wird, weitere Verbesserungen der Bündelgröße voranzutreiben.
Beachten Sie, dass die Reduzierung nicht nur auf das Entfernen ungenutzter Exporte zurückzuführen ist, sondern auch darauf, dass Webpack ModuleConcatenationPluginmehr Module verketten kann. Mit anderen Worten, wir können mehr Module heben . Wir nutzen das Heben des Zielfernrohrs noch nicht vollständig. Derzeit werden nur 20 % der Module gehoben. Wir haben weitere Bündelgrößen- und Laufzeitgewinne erwartet, sobald wir diese Zahl erhöhen.
Die 40-prozentige Reduzierung von JavaScript bildet fast genau 1:1 die Zeit ab, die benötigt wird, um das JavaScript-Bundle zu evaluieren, bevor es ausgeführt werden kann. Das Auswerten von JavaScript blockiert die resultierende Startzeit, sodass die Reduzierung der gelieferten JavaScript-Menge direkt die Startzeit reduziert.
Nachdem der letzte Schritt der Implementierung 2 Wochen später abgeschlossen war, hatten wir immer noch die gleichen Ergebnisse in unserem Labor und waren bereit, dieses Feature in unserer Hauptniederlassung zu landen. Wir haben besonders darauf geachtet, die endgültige Änderung direkt nach unserem Release-Cutoff zu landen. Dadurch konnten wir das neue Modulsystem ausgiebig in internen App-Versionen testen, die von unseren Mitarbeitern genutzt werden. Nach einer Woche interner Tests wurde die Funktion schrittweise für unsere Endbenutzer eingeführt. Wir waren sehr hoffnungsvoll, nachdem wir sahen, dass die App-Stabilität weitgehend unbeeinflusst blieb. Die Produktionsdaten zeigten die gleichen relativen Verbesserungen gegenüber der mittleren Startzeit, die wir in unseren Laborergebnissen gesehen haben:
Version 22.37 ist ohne Tree Shaking; 22.38 Uhr mit Baumschütteln
Diese Verbesserungen gehen zu Lasten verlängerter Bauzeiten. Das Bündeln des Produktions-JavaScript-Bundles dauert etwa 30 % (4 Minuten) länger. Wir nehmen diese längeren Build-Zeiten gerne in Kauf, da sie sich direkt in einer besseren Benutzererfahrung niederschlagen. Einige der verlängerten Bauzeiten werden darauf zurückgeführt, dass mehr als nötig transpiliert wurde. Bei der anfänglichen Implementierung wurde keine Zeit darauf verwendet, die Menge, die wir transpilieren müssen, zu reduzieren. Wir werden auch einige der Erhöhungen der Bauzeit wieder hereinholen, je mehr Pakete richtige ES-Module enthalten. Denken Sie daran, dass die JavaScript-Bündelungszeit nicht die einzige Aufgabe ist, die zum Erstellen einer React Native-App erforderlich ist. Beim Kompilieren von Binärdateien usw. ist die Zunahme der JavaScript-Bündelung am Ende nicht so wirkungsvoll.
Was kommt als nächstes
Es scheint, als ob im React Native-Ökosystem nicht aktiv an ES-Modulen gearbeitet wurde. Wir wollen das Ökosystem mehr auf die richtige Verwendung von ES-Modulen ausrichten (zB moduleEinträge, die auf JavaScript mit äquivalenter Syntax verweisen). Auf diese Weise können wir unsere Build-Konfiguration reduzieren und weniger transpilieren.
Während die Verwendung von ES-Modulen hinter einem Flag in Metro ( experimentalImportSupport) unterstützt wird, ist dies als experimentell gekennzeichnet und nicht dokumentiert. Das Aktivieren dieses Flags in der Entwicklung funktioniert für uns (noch) nicht, aber wir hoffen, dass wir eines Tages dasselbe Modulsystem in Entwicklung und Produktion verwenden können. Wir möchten die Diskussion über ES-Module in React native neu starten, da anscheinend derzeit nicht aktiv an der Unterstützung von ES-Modulen gearbeitet wird. Die Unterstützung von Tree Shaking wurde sogar vor Jahren komplett aufgegeben. .
Schließlich ist ES-Module ein Sprachfeature, das jeder, der JavaScript kennt, irgendwann auch lernt. Wir sehen keinen Grund, warum React Native einen zusätzlichen Lernschritt haben sollte, um das Bundle-Splitting und die Eliminierung von totem Code zu verstehen.
Einmal schreiben, überall laufen!
Hat Ihnen dieser Beitrag gefallen? Folgen Sie Klarna Engineering auf Medium und LinkedIn , um weitere Artikel dieser Art zu erhalten.

![Was ist überhaupt eine verknüpfte Liste? [Teil 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































