ist jetzt verfügbar! Lesen Sie über die neuen Funktionen und Fehlerbehebungen vom November.

VS Code schrumpfen mit Name Mangling

20. Juli 2023 von Matt Bierner, @mattbierner

Wir haben kürzlich die Größe des ausgelieferten JavaScripts von Visual Studio Code um 20 % reduziert. Das entspricht etwas mehr als 3,9 MB Einsparung. Sicher, das ist weniger als bei einigen einzelnen GIFs aus unseren Release Notes, aber das ist immer noch nichts zu verachten! Diese Reduzierung bedeutet nicht nur, dass Sie weniger Code herunterladen und auf der Festplatte speichern müssen, sondern verbessert auch die Startzeit, da weniger Quellcode gescannt werden muss, bevor das JavaScript ausgeführt wird. Nicht schlecht, wenn man bedenkt, dass wir diese Reduzierung ohne Löschen von Code und ohne größere Refactorings in unserer Codebasis erzielt haben. Stattdessen erforderte es lediglich einen neuen Build-Schritt: Name Mangling.

In diesem Beitrag möchte ich darlegen, wie wir diese Optimierungsmöglichkeit identifiziert, Lösungsansätze für das Problem untersucht und schließlich diese 20%ige Größenreduzierung erreicht haben. Ich möchte dies eher als Fallstudie dafür behandeln, wie wir technische Probleme im VS Code-Team angehen, anstatt uns auf die Besonderheiten des Manglings zu konzentrieren. Name Mangling ist ein netter Trick, aber für viele Codebasen möglicherweise nicht lohnenswert, und unser spezifischer Ansatz zum Mangling kann wahrscheinlich verbessert werden (oder ist je nach Aufbau Ihres Projekts gar nicht notwendig).

Identifizierung des Problems

Das VS Code-Team ist leidenschaftlich daran interessiert, die Leistung zu verbessern, sei es durch die Optimierung von Hot Code Paths, die Reduzierung von UI-Relayouts oder die Beschleunigung der Startzeit. Diese Leidenschaft beinhaltet auch die Reduzierung der Größe des JavaScripts von VS Code. Die Code-Größe ist mit dem Versand von VS Code im Web (https://vscode.dev) zusätzlich zur Desktop-Anwendung noch wichtiger geworden. Die aktive Überwachung der Code-Größe hält die Mitglieder des VS Code-Teams über Änderungen auf dem Laufenden.

Leider waren diese Änderungen fast immer Zunahmen. Obwohl wir viel über die Funktionen nachdenken, die wir in VS Code integrieren, hat die Hinzufügung neuer Funktionalitäten im Laufe der Jahre zwangsläufig die Menge des von uns ausgelieferten Codes vergrößert. Zum Beispiel ist eine der Kern-JavaScript-Dateien von VS Code (workbench.js) heute etwa viermal so groß wie vor acht Jahren. Wenn man bedenkt, dass VS Code vor acht Jahren Funktionen fehlten, die heute viele als essentiell betrachten würden – wie Editor-Tabs oder das integrierte Terminal – ist diese Zunahme vielleicht nicht so schlimm, wie sie klingt, aber sie ist nicht nichts.

The size of 'workbench.js' has slowly increased over the past eight years

Diese 4-fache Größensteigerung ist auch nach viel fortlaufender Performance-Engineering-Arbeit. Auch diese Arbeit geschieht weitgehend, weil wir unsere Code-Größe verfolgen und es wirklich nicht mögen, wenn sie zunimmt. Wir haben bereits viele einfache Code-Größen-Optimierungen vorgenommen, darunter das Durchlaufen unseres Codes durch esbuild zur Minifizierung. Weitere Einsparungen zu finden, ist im Laufe der Jahre immer schwieriger geworden. Viele potenzielle Einsparungen sind auch die damit verbundenen Risiken oder der zusätzliche technische Aufwand für Implementierung und Wartung nicht wert. Das bedeutet, dass wir zusehen mussten, wie die Größe unseres JavaScripts langsam nach oben tickte.

Beim Debuggen unseres minifizierten Quellcodes auf vscode.dev im letzten Jahr fiel mir jedoch etwas Überraschendes auf: unser minifiziertes JavaScript enthielt immer noch unzählige lange Bezeichnernamen, wie extensionIgnoredRecommendationsService. Das hat mich überrascht. Ich ging davon aus, dass esbuild diese Bezeichner bereits verkürzt hätte. Und es stellt sich heraus, dass esbuild Bezeichner in einigen Fällen tatsächlich durch einen Prozess namens "Mangling" verkürzt (ein Begriff, den JavaScript-Tools wahrscheinlich von einem nur grob ähnlichen Prozess für kompilierte Sprachen übernommen haben).

Während der Minifizierung verkürzt Mangling lange Bezeichnernamen und transformiert Code wie

const someLongVariableName = 123;
console.log(someLongVariableName);

in das viel kürzere

const x = 123;
console.log(x);

Da JavaScript als Quelltext ausgeliefert wird, verringert die Reduzierung der Länge von Bezeichnernamen tatsächlich die Größe des Programms. Ich weiß, dass diese Optimierung für jemanden, der aus einer kompilierten Sprache kommt, vielleicht mehr als ein wenig albern erscheint, aber hier in der wunderbaren Welt von JavaScript nehmen wir solche Gewinne gerne mit, wo immer wir sie finden können!

Bevor Sie nun eilig alle Ihre Variablen in einzelne Buchstaben umbenennen, möchte ich betonen, dass solche Optimierungen mit Vorsicht angegangen werden müssen. Wenn eine potenzielle Optimierung Ihren Quellcode weniger lesbar oder wartbar macht oder erhebliche manuelle Arbeit erfordert, ist sie fast nie lohnenswert, es sei denn, sie liefert wirklich spektakuläre Verbesserungen. Hier und da ein paar Bytes einzusparen ist nett, qualifiziert aber kaum als spektakulär.

Diese Kalkulation ändert sich, wenn wir nette Optimierungen wie diese praktisch kostenlos erhalten können, zum Beispiel indem unser Build-Tool sie automatisch für uns durchführt. Und tatsächlich implementieren intelligente Tools wie esbuild bereits Identifier Mangling. Das bedeutet, wir können weiterhin unsere veryLongAndDescriptiveNamesThatWouldMakeEvenObjectiveCProgrammersBlush schreiben und unsere Build-Tools sie für uns verkürzen lassen!

Obwohl esbuild Mangling implementiert, verkürzt es standardmäßig Namen nur, wenn es sicher ist, dass das Mangling das Verhalten des Codes nicht ändert. Schließlich ist es wirklich ärgerlich, wenn ein Bundler Ihren Code beschädigt. In der Praxis bedeutet dies, dass esbuild lokale Variablennamen und Argumentnamen verkürzt. Dies ist sicher, es sei denn, Ihr Code tut etwas wirklich Absurdes (in diesem Fall haben Sie wahrscheinlich weitaus größere Probleme als die Code-Größe zu befürchten).

Der konservative Ansatz von esbuild bedeutet jedoch, dass es viele Namen überspringt, da es nicht sicher ist, ob deren Änderung sicher ist. Als einfaches Beispiel dafür, wie Dinge schiefgehen könnten, betrachten Sie

const obj = { longPropertyName: 123 };

function lookup(prop) {
  return obj[prop];
}

console.log(lookup('longPropertyName'));

Wenn Mangling longPropertyName in x ändert, funktioniert die dynamische Suche in der nächsten Zeile nicht mehr

const obj = { x: 123 }; // Here `longPropertyName` gets rewritten to `x`

function lookup(prop) {
  return obj[prop];
}

console.log(lookup('longPropertyName')); // But this reference doesn't and now the lookup is broken

Beachten Sie im obigen Code, wie wir immer noch longPropertyName verwenden, um auf die Eigenschaft zuzugreifen, obwohl die Eigenschaft selbst beim Mangling geändert wurde.

Obwohl dieses Beispiel konstruiert ist, gibt es tatsächlich viele Möglichkeiten, wie diese Brüche in realem Code auftreten können

  • Dynamischer Eigenschaftszugriff.
  • Serialisieren von Objekten oder Parsen von JSON in eine erwartete Objektform.
  • Von Ihnen bereitgestellte APIs (die Konsumenten kennen die neuen gemangelten Namen nicht.)
  • Von Ihnen verwendete APIs (einschließlich DOM-APIs.)

Obwohl Sie esbuild zwingen können, praktisch jeden gefundenen Namen zu mangeln, bricht dies VS Code aus den oben genannten Gründen vollständig.

Trotzdem konnte ich das Gefühl nicht loswerden, dass wir im VS Code-Codebase doch etwas besser machen könnten. Wenn wir nicht jeden Namen mangeln könnten, könnten wir vielleicht zumindest eine Teilmenge von Namen finden, die wir sicher mangeln könnten.

Falsche Starts mit privaten Eigenschaften

Beim Rückblick auf unsere minifizierten Quellen fiel mir noch etwas auf: viele lange Namen, die mit _ begannen. Konventionell kennzeichnet dies eine private Eigenschaft. Sicherlich können private Eigenschaften sicher gemangelt werden und Code außerhalb der Klasse würde nichts davon bemerken, richtig? Und warten Sie mal, sollte esbuild das nicht schon für uns tun? Doch ich wusste, dass die Leute, die esbuild geschrieben haben, keine schlechten Leute waren. Wenn esbuild keine privaten Eigenschaften mangelte, dann fast sicherlich aus gutem Grund.

Als ich weiter über das Problem nachdachte, erkannte ich, dass private Eigenschaften vom selben dynamischen Eigenschaftszugriffsproblem betroffen sind, wie im Beispiel longPropertyName oben gezeigt. Ich bin sicher, ein intelligenter TypeScript-Programmierer wie Sie würde niemals solchen Code schreiben, aber dynamische Muster sind in realen Codebasen häufig genug, dass esbuild vorsichtig ist.

Denken Sie auch daran, dass das Schlüsselwort private in TypeScript wirklich nur ein höflicher Vorschlag ist. Wenn TypeScript-Code zu JavaScript kompiliert wird, wird das Schlüsselwort private praktisch entfernt. Das bedeutet, dass nichts den Zugriff auf private Eigenschaften von unhöflichem Code außerhalb der Klasse verhindert.

class Foo {
  private bar = 123;
}

const foo: any = new Foo();
console.log(foo.bar);

Hoffentlich tut Ihr Code solche fragwürdigen Dinge nicht direkt, aber das unachtsame Ändern von Eigenschaftsnamen kann Sie auf viele unerwartete Arten beißen, wie z. B. bei Objekt-Spreads, Serialisierung und wenn unterschiedliche Klassen gemeinsame Eigenschaftsnamen haben.

Glücklicherweise erkannte ich, dass ich mit VS Code einen riesigen Vorteil hatte: Ich arbeitete mit einer (größtenteils) gesunden Codebasis. Ich konnte viele Annahmen treffen, die esbuild nicht treffen konnte, wie z. B. dass es keine dynamischen Zugriffe auf private Eigenschaften oder schlechte any-Zugriffe gibt. Dies vereinfachte das Problem, vor dem ich stand, weiter.

Also begannen Johannes Rieken (@johannesrieken) und ich, das Mangling privater Eigenschaften zu untersuchen. Unsere erste Idee war, überall in unserer Codebasis JavaScripts native #private-Felder zu übernehmen. Nicht nur sind private Felder immun gegen all die oben genannten Probleme, sie werden von esbuild bereits automatisch gemangelt. Die Annäherung an reines JavaScript war ebenfalls attraktiv.

Allerdings verwarfen wir diesen Ansatz schnell, da er massive (und damit riskante) Codeänderungen erfordern würde, einschließlich der Entfernung all unserer Verwendungen von Parameter Properties. Als relativ neue Funktion wurden private Felder noch nicht über alle Laufzeiten hinweg optimiert. Ihre Verwendung kann zu Verlangsamungen führen, die von vernachlässigbar bis zu etwa 95 % reichen! Obwohl dies langfristig die richtige Änderung sein mag, war es nicht das, was wir gerade brauchten.

Als nächstes entdeckten wir, dass esbuild selektiv Eigenschaften mangeln kann, die einem gegebenen regulären Ausdruck entsprechen. Dieser reguläre Ausdruck gleicht jedoch nur den Bezeichnernamen ab. Obwohl dies bedeutete, dass wir nicht wissen konnten, ob die Eigenschaft in TypeScript als private deklariert war, könnten wir versuchen, alle Eigenschaften zu mangeln, die mit _ beginnen, von denen wir hofften, dass sie nur private und geschützte Eigenschaften enthalten würden.

Schon bald hatten wir einen funktionierenden Build mit allen gemangelten _-Eigenschaften. Schön! Dies bewies, dass Mangling privater Eigenschaften möglich war und einige anständige Einsparungen brachte, wenn auch weit weniger, als wir uns erhofft hatten.

Leider hat das Mangling, das nur auf Namen basiert, einige ernsthafte Nachteile, darunter die Anforderung, dass alle privaten Eigenschaften in unserer Codebasis mit _ beginnen müssen. Die VS Code-Codebasis folgt dieser Namenskonvention nicht durchgängig, und es gibt auch einige Stellen, an denen wir öffentliche Eigenschaften haben, die mit _ beginnen (typischerweise geschieht dies, wenn eine Eigenschaft extern zugänglich sein muss, aber nicht als API behandelt werden soll, z. B. in Tests).

Wir waren uns auch nicht ganz sicher, dass der gemangelte Code tatsächlich korrekt war. Sicher, wir konnten unsere Tests ausführen oder versuchen, VS Code zu starten, aber das war zeitaufwendig und was, wenn wir weniger häufige Code-Pfade übersehen hätten? Wir konnten nicht zu 100 % sicher sein, dass wir nur private Eigenschaften mangelten, ohne anderen Code zu berühren. Dieser Ansatz schien sowohl zu riskant als auch zu mühsam, um ihn zu übernehmen.

Vertrauensvolles Mangling mit TypeScript

Als wir darüber nachdachten, wie wir uns bei einem Mangling-Build-Schritt sicherer fühlen könnten, kamen wir auf eine neue Idee: Was wäre, wenn TypeScript den gemangelten Code für uns überprüfen könnte? So wie TypeScript unbekannte Eigenschaftszugriffe im normalen Code erkennen kann, sollte der TypeScript-Compiler Fälle erkennen können, in denen eine Eigenschaft gemangelt wurde, aber Referenzen darauf nicht korrekt aktualisiert wurden. Anstatt das kompilierte JavaScript zu mangeln, könnten wir stattdessen unseren TypeScript-Quellcode mangeln und dann den neuen TypeScript mit den gemangelten Bezeichnernamen kompilieren. Der Kompilierungsschritt für den gemangelten Quellcode würde uns mehr Vertrauen geben, dass wir unseren Code nicht versehentlich beschädigt haben.

Nicht nur das, sondern durch die Verwendung von TypeScript könnten wir wirklich alle private-Eigenschaften finden (anstelle von Eigenschaften, die nur mit _ beginnen). Wir könnten sogar die bestehende rename-Funktionalität von TypeScript verwenden, um Symbole intelligent umzubenennen, ohne Objektformen unerwartet zu verändern.

Eifrig, diesen neuen Ansatz auszuprobieren, entwickelten wir bald einen neuen Mangling-Build-Schritt, der grob wie folgt funktioniert:

for each private or protected property in codebase (found using TypeScript's AST):
    if the property should be mangled:
        Compute a new name by looking for an unused symbol name
        Use TypeScript to generate a rename edit for all references to the property

Apply all rename edits to our typescript source

Compile the new edited TypeScript sources with the mangled names

Und einigermaßen überraschend für einen so naiv erscheinenden Ansatz hat es funktioniert! Nun, meistens zumindest.

Wir waren zwar definitiv beeindruckt davon, wie gut TypeScript Tausende und Abertausende von korrekten Änderungen in unserer gesamten Codebasis generieren konnte, mussten aber auch Logik hinzufügen, um einige Randfälle zu behandeln:

  • Es reicht nicht aus, dass ein neuer privater Eigenschaftsname in der aktuellen Klasse eindeutig ist, er muss auch über alle Ober- und Unterklassen der aktuellen Klasse hinweg eindeutig sein. Wieder ist die Ursache, dass das private-Schlüsselwort von TypeScript nur eine Laufzeitdekoration ist, die nicht wirklich durchsetzt, dass Ober- und Unterklassen nicht auf private Eigenschaften zugreifen können. Ohne Sorgfalt kann das Umbenennen Namenskollisionen verursachen (glücklicherweise meldet TypeScript diese als Fehler).

  • An einigen Stellen in unserem Code haben Unterklassen geerbte geschützte Eigenschaften öffentlich gemacht. Obwohl viele davon Fehler waren, haben wir auch Code hinzugefügt, um das Mangling in diesen Fällen zu deaktivieren.

Nachdem wir Code für diese Fälle hinzugefügt hatten, hatten wir bald funktionierende Builds. Durch das Mangling privater Eigenschaften sank die Größe des Haupt-workbench.js-Skripts von VS Code von 12,3 MB auf 10,6 MB, eine Reduzierung von fast 14 %. Dies brachte auch eine 5%ige Beschleunigung der Code-Ladung, da weniger Quelltext gescannt werden muss. Gar nicht schlecht, wenn man bedenkt, dass diese Einsparungen, abgesehen von ein paar sehr kleinen Korrekturen an unsicheren Mustern in unseren Quellen, praktisch kostenlos waren.

Erkenntnisse und weitere Arbeit

Das Mangling privater Eigenschaften zeigt, dass immer noch bedeutende Verbesserungen in VS Code erzielt werden können, ohne auf massive Codeänderungen oder kostspielige Neufassungen zurückgreifen zu müssen. In diesem Fall vermute ich, dass andere im Laufe der Jahre VS Codes minifizierte Quellen durchgesehen und sich über diese langen Namen gewundert haben. Die Bewältigung dieses Problems schien jedoch wahrscheinlich unmöglich sicher durchzuführen oder war vielleicht einfach nicht die potenziell massive technische Investition wert.

Der Schlüssel zu unserem Erfolg war diesmal die Identifizierung eines Falls (private Eigenschaften), bei dem das Namens-Mangling wahrscheinlich sicher wäre und die Optimierung immer noch eine bedeutende Verbesserung bringen würde. Dann überlegten wir, wie wir diese Änderung so sicher wie möglich gestalten könnten. Das bedeutete zuerst die Verwendung der TypeScript-Tools, um Bezeichner vertrauensvoll umzubenennen, und dann wieder die Verwendung von TypeScript, um sicherzustellen, dass unser neu gemangelter Quellcode weiterhin korrekt kompiliert wird. Unterwegs halfen uns die Tatsache, dass unser Code bereits den meisten TypeScript-Best-Practices folgte und Tests vorhanden waren, die viele der gängigen VS Code-Code-Pfade abdecken, sehr. All dies führte dazu, dass Joh und ich in unserer Freizeit eine ziemlich drastische Änderung mit fast keinen Auswirkungen auf die anderen Entwickler, die an VS Code arbeiten, umsetzen konnten.

Das ist jedoch noch nicht das Ende der Mangling-Geschichte. Beim Durchsehen unserer neu gemangelten und minifizierten Quellen sah ich enttäuscht provideWorkspaceTrustExtensionProposals und viele andere sperrige Namen. Das Bemerkenswerteste waren die fast 5000 Vorkommen von localize (die Funktion, die wir für in der Benutzeroberfläche angezeigte Strings verwenden). Klar, es gab noch Raum für Verbesserungen.

Mit demselben Ansatz und denselben Techniken wie beim Mangling privater Eigenschaften identifizierte ich bald ein weiteres häufiges Code-Muster, das wir sicher mit hohem ROI mangeln konnten: exportierte Symbolnamen. Solange die Exporte nur intern verwendet wurden, war ich zuversichtlich, dass wir sie kürzen konnten, ohne das Verhalten des Codes zu ändern.

Dies erwies sich größtenteils als richtig, obwohl es wieder einige Komplikationen gab. Zum Beispiel mussten wir sicherstellen, dass wir die von Erweiterungen verwendeten APIs nicht versehentlich berühren, und einige Symbole ausnehmen, die von TypeScript exportiert, aber dann aus untypisiertem JavaScript aufgerufen wurden (typischerweise sind dies Einstiegspunkte für einen Worker-Thread oder -Prozess).

Das Export-Mangling wurde in der letzten Iteration bereitgestellt und reduzierte die Größe von workbench.js weiter von 10,6 MB auf 9,8 MB. Insgesamt ist diese Datei jetzt 20 % kleiner als ohne Mangling. Über ganz VS Code hinweg entfernt Mangling 3,9 MB JavaScript-Code aus unseren kompilierten Quellen. Das ist nicht nur eine schöne Reduzierung der Download- und Installationsgröße, sondern auch 3,9 MB weniger JavaScript, das bei jedem Start von VS Code gescannt werden muss.

Diese Grafik zeigt die Größe von workbench.js im Laufe der Zeit. Beachten Sie die beiden Rückgänge auf der rechten Seite. Der erste große Rückgang in VS Code 1.74 ist das Ergebnis des Manglings privater Eigenschaften. Der zweite kleinere Rückgang in 1.80 ist auf das Mangling von Exporten zurückzuführen.

Zoomed in chart showing the drops from mangling

The size of 'workbench.js' over all VS Code releases, including the mangling work

Unsere Mangling-Implementierung kann zweifellos verbessert werden, da unsere minifizierten Quellen immer noch viele lange Namen enthalten. Wir werden diese möglicherweise weiter untersuchen, wenn sich dies als lohnenswert erweist und wir einen sicheren Ansatz finden können. Idealerweise wird ein Großteil dieser Arbeit eines Tages gar nicht mehr notwendig sein. Native private Eigenschaften werden bereits automatisch gemangelt und unsere Build-Tools werden hoffentlich besser darin, Code in unserer gesamten Codebasis zu optimieren. Sie können unsere aktuelle Mangling-Implementierung überprüfen.

Wir streben ständig danach, VS Code und unsere Codebasis zu verbessern, und ich denke, die Mangling-Arbeit ist ein großartiges Beispiel dafür, wie wir dies angehen. Optimierung ist ein fortlaufender Prozess, keine einmalige Sache. Durch die kontinuierliche Überwachung unserer Code-Größe waren wir uns bewusst, wie sie im Laufe der Zeit gewachsen ist. Dieses Bewusstsein hat zweifellos dazu beigetragen, dass unsere Code-Größe nicht noch weiter zugenommen hat, und ermutigt uns auch, ständig nach Verbesserungen Ausschau zu halten. Obwohl Mangling eine attraktiv erscheinende Technik war, war sie anfangs zu riskant, um sie ernsthaft in Betracht zu ziehen. Erst als wir daran gearbeitet hatten, dieses Risiko zu mindern, die richtigen Sicherheitsnetze zu schaffen und die Kosten für die Einführung von Mangling fast auf Null zu reduzieren, fühlten wir uns endlich zuversichtlich genug, es in unsere Builds zu integrieren. Ich bin wirklich stolz auf das Endergebnis und genauso stolz darauf, wie wir es erreicht haben.

Viel Spaß beim Programmieren,

Matt Bierner, Mitglied des VS Code Teams @mattbierner


Vielen Dank an Johannes Rieken für seine entscheidende Arbeit bei der Implementierung von Mangling, an das TypeScript-Team für die Entwicklung der Tools, die uns die sichere Implementierung von Mangling ermöglicht haben, an esbuild für ihren blitzschnellen Bundler und an das gesamte VS Code-Team für die Entwicklung einer Codebasis, die für solche Optimierungen geeignet ist. Und nicht zuletzt ein riesiges Dankeschön an das V8-Team und alle anderen JS-Engines dafür, dass sie uns immer schnell aussehen lassen, trotz der Haufen und Haufen von furchtbar gemangeltem JavaScript, das wir ihnen entgegenwerfen.

© . This site is unofficial and not affiliated with Microsoft.