Transcript

00:00:00(fröhliche Musik) - Okay, vielen Dank, hallo zusammen.
00:00:07Mein Name ist Luke Sandberg.
00:00:09Ich bin Software-Ingenieur bei Vercel und arbeite an Turbo Pack.
00:00:12Ich bin seit etwa sechs Monaten bei Vercel,
00:00:14was mir gerade genug Zeit gab,
00:00:16hier auf die Bühne zu kommen und Ihnen von all der großartigen Arbeit zu erzählen,
00:00:21die ich nicht geleistet habe.
00:00:23Vor meiner Zeit bei Vercel war ich bei Google,
00:00:26wo ich an unseren internen Web-Toolchains arbeiten und seltsame Dinge tun konnte,
00:00:31wie einen TSX-zu-Java-Bytecode-Compiler zu bauen und am Closure Compiler zu arbeiten.
00:00:37Als ich bei Vercel ankam,
00:00:39war es tatsächlich,
00:00:40als würde ich auf einem anderen Planeten landen – alles war anders.
00:00:45Und ich war ziemlich überrascht von all den Dingen,
00:00:47die wir im Team taten,
00:00:48und den Zielen,
00:00:49die wir hatten.
00:00:50Heute werde ich Ihnen einige der Designentscheidungen vorstellen,
00:00:53die wir bei Turbo Pack getroffen haben,
00:00:55und wie ich glaube,
00:00:56dass sie uns ermöglichen werden,
00:00:58auf der bereits fantastischen Leistung aufzubauen.
00:01:01Um das zu verdeutlichen,
00:01:02hier ist unser übergeordnetes Designziel.
00:01:06Daraus lässt sich sofort ableiten,
00:01:08dass wir wahrscheinlich einige schwierige Entscheidungen getroffen haben.
00:01:14Was ist also mit Kalt-Builds?
00:01:17Die sind wichtig,
00:01:18aber eine unserer Ideen ist,
00:01:20dass Sie sie überhaupt nicht erleben sollten.
00:01:22Und darauf wird sich dieser Vortrag konzentrieren.
00:01:24In der Keynote haben Sie ein wenig darüber gehört,
00:01:27wie wir Inkrementalität nutzen,
00:01:29um die Bundling-Leistung zu verbessern.
00:01:31Eine Schlüsselidee,
00:01:32die wir für Inkrementalität haben,
00:01:34ist das Caching.
00:01:35Wir wollen alles,
00:01:36was der Bundler tut,
00:01:37cachebar machen,
00:01:38sodass wir bei jeder Änderung nur die Arbeit wiederholen müssen,
00:01:41die mit dieser Änderung zusammenhängt.
00:01:43Oder anders ausgedrückt: Die Kosten Ihres Builds sollten wirklich mit der Größe oder Komplexität Ihrer Änderung skalieren und nicht mit der Größe oder Komplexität Ihrer Anwendung.
00:01:53Und so können wir sicherstellen,
00:01:55dass Turbo Pack Entwicklern weiterhin eine gute Leistung bietet,
00:01:58egal wie viele Icon-Bibliotheken Sie importieren.
00:02:01Um diese Idee zu verstehen und zu motivieren,
00:02:04stellen wir uns den einfachsten Bundler der Welt vor,
00:02:07der vielleicht so aussieht.
00:02:09Hier ist also unser Baby-Bundler.
00:02:12Und das ist vielleicht etwas zu viel Code für eine Folie,
00:02:15aber es wird noch schlimmer werden.
00:02:17Hier parsen wir also jeden Entry Point.
00:02:20Wir verfolgen ihre Imports,
00:02:21lösen ihre Referenzen rekursiv durch die gesamte Anwendung auf,
00:02:25um alles zu finden,
00:02:26wovon Sie abhängen.
00:02:28Am Ende sammeln wir dann einfach alles,
00:02:30wovon jeder Entry Point abhängt,
00:02:32und werfen es in eine Ausgabedatei.
00:02:35Hurra, wir haben einen Baby-Bundler.
00:02:38Das ist natürlich naiv,
00:02:40aber wenn wir es aus inkrementeller Sicht betrachten,
00:02:43ist kein Teil davon inkrementell.
00:02:45Wir werden also bestimmte Dateien definitiv mehrmals parsen,
00:02:49vielleicht je nachdem,
00:02:50wie oft Sie sie importieren – das ist furchtbar.
00:02:53Wir werden den React-Import definitiv Hunderte oder Tausende Male auflösen.
00:02:57Also, wissen Sie, autsch.
00:03:01Wenn wir das also zumindest ein wenig inkrementeller gestalten wollen,
00:03:04müssen wir einen Weg finden,
00:03:06redundante Arbeit zu vermeiden.
00:03:08Fügen wir also einen Cache hinzu.
00:03:10Man könnte sich vorstellen,
00:03:12dass dies unsere Parse-Funktion ist.
00:03:15Sie ist ziemlich einfach.
00:03:15Und sie ist wahrscheinlich so etwas wie das Arbeitstier unseres Bundlers.
00:03:19Wissen Sie, sehr einfach.
00:03:19Wir lesen den Dateiinhalt,
00:03:21übergeben ihn an SWC,
00:03:23um einen AST zu erhalten.
00:03:25Fügen wir also einen Cache hinzu.
00:03:27Okay, das ist eindeutig ein schöner, einfacher Gewinn.
00:03:31Aber,
00:03:32wissen Sie,
00:03:32ich bin sicher,
00:03:33einige von Ihnen haben schon einmal Caching-Code geschrieben.
00:03:36Vielleicht gibt es hier einige Probleme.
00:03:38Zum Beispiel, was ist, wenn sich die Datei ändert?
00:03:41Das ist eindeutig etwas, das uns wichtig ist.
00:03:46Und,
00:03:46wissen Sie,
00:03:47was ist,
00:03:47wenn die Datei gar keine echte Datei ist,
00:03:50sondern drei Symlinks in einem Trenchcoat?
00:03:52Viele Paketmanager organisieren Abhängigkeiten so.
00:03:55Und wir verwenden den Dateinamen als Cache-Schlüssel.
00:03:59Ist das genug?
00:04:00Zum Beispiel, wir bündeln für den Client und den Server.
00:04:03Dieselbe Dateien landen in beiden.
00:04:04Funktioniert das?
00:04:05Wir speichern auch den AST und geben ihn zurück.
00:04:08Jetzt müssen wir uns also um Mutationen kümmern.
00:04:11Also,
00:04:11wissen Sie,
00:04:12und schließlich,
00:04:13ist das nicht eine wirklich naive Art zu parsen?
00:04:16Ich weiß,
00:04:16dass jeder massive Konfigurationen für den Compiler hat.
00:04:21Einiges davon muss hier rein.
00:04:23Also, ja, das ist alles großartiges Feedback.
00:04:27Und das ist ein sehr naiver Ansatz.
00:04:32Und dazu würde ich natürlich sagen: Ja,
00:04:34das wird nicht funktionieren.
00:04:36Was tun wir also, um diese Probleme zu beheben?
00:04:39Bitte beheben Sie das und machen Sie keine Fehler.
00:04:44Also, okay.
00:04:46Also, vielleicht ist das ein bisschen besser.
00:04:49Wissen Sie,
00:04:49Sie können hier sehen,
00:04:50dass wir einige Transforms haben.
00:04:52Wir müssen angepasste Dinge für jede Datei tun,
00:04:54wie zum Beispiel Down-Leveling oder die Implementierung von `use cache`.
00:04:58Wir haben auch eine Konfiguration.
00:05:00Und so müssen wir das natürlich in unseren Schlüssel für unseren Cache aufnehmen.
00:05:04Aber vielleicht sind Sie sofort misstrauisch.
00:05:08Zum Beispiel, ist das korrekt?
00:05:09Ist es zum Beispiel tatsächlich ausreichend,
00:05:11einen Transform anhand des Namens zu identifizieren?
00:05:13Ich weiß nicht,
00:05:13vielleicht hat das eine eigene komplizierte Konfiguration.
00:05:16Und,
00:05:17okay,
00:05:17und wird dieser JSON-Wert tatsächlich alles erfassen,
00:05:22was uns wichtig ist?
00:05:24Werden die Entwickler es pflegen?
00:05:26Wie groß werden diese Cache-Schlüssel sein?
00:05:29Wie viele Kopien der Konfiguration werden wir haben?
00:05:31Ich habe persönlich schon genau solchen Code gesehen,
00:05:34und ich finde es fast unmöglich,
00:05:36ihn zu durchschauen.
00:05:37Okay,
00:05:37wir haben auch versucht,
00:05:39dieses andere Problem mit Invalidierungen zu beheben.
00:05:43Also haben wir eine Callback-API zum Lesen von Dateien hinzugefügt.
00:05:46Das ist großartig,
00:05:47denn wenn sich die Datei ändert,
00:05:49können wir sie einfach aus dem Cache löschen,
00:05:51damit wir keine veralteten Inhalte mehr bereitstellen.
00:05:55Okay,
00:05:55aber das ist eigentlich ziemlich naiv,
00:05:57denn klar,
00:05:57wir müssen unseren Cache löschen,
00:05:59aber unser Aufrufer muss auch wissen,
00:06:01dass er eine neue Kopie erhalten muss.
00:06:03Also, okay, fangen wir an, Callbacks durchzureichen.
00:06:06Okay, wir haben es geschafft.
00:06:09Wir haben Callbacks durch den Stack nach oben gereicht.
00:06:12Sie können hier sehen,
00:06:13dass wir unserem Aufrufer erlauben,
00:06:15Änderungen zu abonnieren.
00:06:16Wir können das gesamte Bundle einfach erneut ausführen,
00:06:19wenn sich etwas ändert,
00:06:20und wenn sich eine Datei ändert,
00:06:21rufen wir es auf.
00:06:22Großartig, wir haben einen reaktiven Bundler.
00:06:25Aber das ist immer noch kaum inkrementell.
00:06:28Wenn sich also eine Datei ändert,
00:06:31müssen wir alle Module erneut durchlaufen und alle Ausgabedateien erzeugen.
00:06:37Also,
00:06:38wissen Sie,
00:06:38wir haben eine Menge Arbeit gespart,
00:06:41indem wir unseren Parse-Cache hatten,
00:06:43aber das ist nicht wirklich genug.
00:06:45Und dann schließlich gibt es all diese andere redundante Arbeit.
00:06:49Zum Beispiel wollen wir die Imports definitiv cachen.
00:06:52Wir finden eine Datei vielleicht viele Male,
00:06:53und wir brauchen immer wieder ihre Imports,
00:06:55also wollen wir dort einen Cache einrichten.
00:06:57Und,
00:06:58wissen Sie,
00:06:58Resolve-Ergebnisse sind tatsächlich ziemlich kompliziert,
00:07:01also sollten wir das definitiv cachen,
00:07:02damit wir die Arbeit,
00:07:03die wir beim Auflösen von React geleistet haben,
00:07:06wiederverwenden können.
00:07:08Aber, okay, jetzt haben wir ein weiteres Problem.
00:07:11Ihre Resolve-Ergebnisse ändern sich,
00:07:13wenn Sie Abhängigkeiten aktualisieren oder neue Dateien hinzufügen,
00:07:16also brauchen wir dort einen weiteren Callback.
00:07:18Und wir wollen definitiv auch die Logik zum Erzeugen von Outputs cachen,
00:07:22denn wenn man es in einer HMR-Sitzung betrachtet,
00:07:25bearbeitet man einen Teil der Anwendung,
00:07:28warum schreiben wir also jedes Mal alle Outputs neu?
00:07:31Und außerdem könnten Sie eine Ausgabedatei löschen,
00:07:34also sollten wir wahrscheinlich auch dort auf Änderungen achten.
00:07:39Okay,
00:07:39vielleicht haben wir all diese Dinge gelöst,
00:07:42aber wir haben immer noch dieses Problem,
00:07:44dass wir jedes Mal,
00:07:45wenn sich etwas ändert,
00:07:47von vorne anfangen.
00:07:48Also,
00:07:48der gesamte Kontrollfluss dieser Funktion funktioniert nicht,
00:07:51denn wenn sich eine einzelne Datei ändert,
00:07:53würden wir wirklich gerne mitten in diese For-Schleife springen.
00:07:56Und dann schließlich ist unsere API für unseren Aufrufer auch hoffnungslos naiv.
00:08:03Sie wollen wahrscheinlich tatsächlich wissen,
00:08:04welche Datei sich geändert hat,
00:08:05damit sie Updates an den Client pushen können.
00:08:07Also, ja.
00:08:11Dieser Ansatz funktioniert also nicht wirklich.
00:08:13Und selbst wenn wir irgendwie alle Callbacks an all diesen Stellen durchgereicht hätten,
00:08:18glauben Sie,
00:08:19dass Sie diesen Code tatsächlich pflegen könnten?
00:08:21Glauben Sie, Sie könnten eine neue Funktion hinzufügen?
00:08:24Ich nicht.
00:08:25Ich denke, das würde einfach abstürzen und scheitern.
00:08:28Und, wissen Sie, dazu würde ich sagen: Ja.
00:08:34Also, noch einmal, was sollen wir tun?
00:08:36Wissen Sie,
00:08:37genau wie beim Chatten mit einem LLM müssen Sie zuerst wissen,
00:08:42was Sie wollen.
00:08:43Und dann müssen Sie es extrem klar formulieren.
00:08:48Also, was wollen wir überhaupt?
00:08:50Also,
00:08:50wissen Sie,
00:08:51wir haben viele verschiedene Ansätze in Betracht gezogen,
00:08:54und viele Leute im Team hatten tatsächlich viel Erfahrung mit Bundlern.
00:08:59Also haben wir diese Art von groben Anforderungen entwickelt.
00:09:02Wir wollen also definitiv jede teure Operation im Bundler cachen können.
00:09:05Und das sollte wirklich einfach sein.
00:09:08Zum Beispiel sollten Sie nicht 15 Kommentare zu Ihrem Code-Review erhalten,
00:09:11jedes Mal,
00:09:11wenn Sie einen neuen Cache hinzufügen.
00:09:12Und dann vertraue ich Entwicklern eigentlich nicht wirklich zu,
00:09:17korrekte Cache-Schlüssel zu schreiben oder Inputs oder Abhängigkeiten manuell zu verfolgen.
00:09:24Also sollten wir uns darum kümmern.
00:09:26Wir sollten das definitiv narrensicher machen.
00:09:30Als Nächstes müssen wir mit sich ändernden Inputs umgehen.
00:09:33Das ist eine große Idee in HMR,
00:09:34aber auch über Sitzungen hinweg.
00:09:36Also,
00:09:36meistens werden das Dateien sein,
00:09:38aber es könnten auch Dinge wie Konfigurationseinstellungen sein.
00:09:40Und mit dem Dateisystem-Cache sind es am Ende tatsächlich auch Dinge wie Umgebungsvariablen.
00:09:45Wir wollen also reaktiv sein.
00:09:47Wir wollen Dinge neu berechnen können,
00:09:49sobald sich etwas ändert,
00:09:51und wir wollen nicht überall Callbacks durchreichen.
00:09:54Schließlich müssen wir einfach moderne Architekturen nutzen und Multi-Threaded und generell schnell sein.
00:10:02Also,
00:10:03vielleicht schauen Sie sich diese Anforderungen an,
00:10:07und einige von Ihnen denken: Was hat das mit einem Bundler zu tun?
00:10:12Und dazu würde ich sagen,
00:10:13natürlich ist mein Management-Team im Raum,
00:10:16also müssen wir darüber nicht wirklich sprechen.
00:10:20Aber wirklich,
00:10:21ich schätze,
00:10:21viele von Ihnen sind zu der viel offensichtlicheren Schlussfolgerung gesprungen.
00:10:24Das klingt sehr nach Signals.
00:10:28Und ja, ich beschreibe ein System, das nach Signals klingt.
00:10:31Es ist eine Möglichkeit,
00:10:32Berechnungen zu komponieren,
00:10:33Abhängigkeiten zu verfolgen,
00:10:35mit einem gewissen Grad an automatischer Memoization.
00:10:37Und ich sollte anmerken,
00:10:39dass wir uns von allen möglichen Systemen inspirieren ließen,
00:10:42insbesondere vom Rust-Compiler und einem System namens Salsa.
00:10:45Und es gibt sogar eine akademische Literatur zu diesen Konzepten,
00:10:48die Adaptons genannt werden,
00:10:49falls Sie interessiert sind.
00:10:51Okay,
00:10:51schauen wir uns an,
00:10:53wie das in der Praxis aussieht,
00:10:55und dann machen wir einen sehr abrupten Sprung von Codebeispielen in JavaScript zu Rust.
00:11:01Hier ist also ein Beispiel für die Infrastruktur,
00:11:03die wir gebaut haben.
00:11:05Eine TurboTask-Funktion ist eine gecachte Arbeitseinheit in unserem Compiler.
00:11:12Sobald Sie eine Funktion so annotieren,
00:11:15können wir sie verfolgen,
00:11:17einen Cache-Schlüssel aus ihren Parametern konstruieren,
00:11:20und das ermöglicht es uns,
00:11:22sie sowohl zu cachen als auch bei Bedarf erneut auszuführen.
00:11:28Diese VC-Typen hier können Sie sich wie Signals vorstellen,
00:11:32das ist ein reaktiver Wert,
00:11:34VC steht für Value Cell,
00:11:35aber Signal wäre vielleicht ein etwas besserer Name.
00:11:39Wenn Sie einen Parameter so deklarieren,
00:11:41sagen Sie: Das könnte sich ändern,
00:11:44ich möchte es erneut ausführen,
00:11:46wenn es sich ändert.
00:11:47Und woher wissen wir das?
00:11:49Wir lesen diese Werte also über ein Await.
00:11:52Sobald Sie einen reaktiven Wert wie diesen awaiten,
00:11:55verfolgen wir die Abhängigkeit automatisch.
00:11:58Und dann schließlich führen wir natürlich die eigentliche Berechnung durch,
00:12:03die wir wollten,
00:12:04und speichern sie in einer Zelle.
00:12:07Da wir Abhängigkeiten automatisch verfolgt haben,
00:12:10wissen wir,
00:12:11dass diese Funktion sowohl vom Inhalt der Datei als auch vom Wert der Konfiguration abhängt.
00:12:17Und jedes Mal,
00:12:18wenn wir ein neues Ergebnis in der Zelle speichern,
00:12:20können wir es mit dem vorherigen vergleichen,
00:12:23und wenn es sich geändert hat,
00:12:24können wir Benachrichtigungen an alle weitergeben,
00:12:27die diesen Wert gelesen haben.
00:12:29Dieses Konzept der Änderung ist also entscheidend für unseren Ansatz zur Inkrementalität.
00:12:33Und ja, der einfachste Fall ist wieder hier.
00:12:37Wenn sich die Datei ändert,
00:12:39wird Turbo Pack das bemerken,
00:12:40diese Funktionsausführung invalidieren und sie sofort erneut ausführen.
00:12:45Und wenn wir dann zufällig denselben AST erzeugen,
00:12:48hören wir einfach dort auf,
00:12:50weil wir dieselbe Zelle berechnen.
00:12:53Nun,
00:12:54beim Parsen einer Datei gibt es kaum eine Änderung,
00:12:56die Sie daran vornehmen können,
00:12:58die den AST nicht tatsächlich ändert.
00:13:00Aber wir können die grundlegende Komponierbarkeit von Turbo Pack-Funktionen nutzen,
00:13:05um dies weiter voranzutreiben.
00:13:07Hier sehen wir also eine weitere Turbo Pack Cache-Funktion,
00:13:12die Imports aus einem Modul extrahiert.
00:13:15Man kann sich vorstellen,
00:13:17dass dies eine sehr häufige Aufgabe ist,
00:13:19die wir im Bundler haben.
00:13:20Wir müssen Imports extrahieren,
00:13:22nur um tatsächlich alle Module in Ihrer Anwendung zu finden.
00:13:25Wir nutzen sie,
00:13:25um den besten Weg zu finden,
00:13:27Module zu Chunks zusammenzufassen.
00:13:29Und natürlich ist der Import-Graph wichtig für grundlegende Aufgaben wie Tree Shaking.
00:13:34Und da es so viele verschiedene Konsumenten der Importdaten gibt,
00:13:39macht ein Cache viel Sinn.
00:13:41Diese Implementierung ist also nicht wirklich besonders.
00:13:44Das ist so,
00:13:44wie man es in jeder Art von Bundler finden würde.
00:13:46Wir durchlaufen den AST,
00:13:48sammeln Imports in einer speziellen Datenstruktur,
00:13:51die uns gefällt,
00:13:52und geben sie dann zurück.
00:13:55Aber die Schlüsselidee hier ist,
00:13:56dass wir sie in einer anderen Zelle speichern.
00:13:58Wenn sich das Modul also ändert,
00:14:00müssen wir diese Funktion erneut ausführen,
00:14:03weil wir sie gelesen haben.
00:14:05Aber wenn Sie über die Art von Änderungen nachdenken,
00:14:08die Sie an Modulen vornehmen,
00:14:09betreffen nur sehr wenige davon tatsächlich die Imports.
00:14:12Sie ändern also das Modul,
00:14:14aktualisieren den Funktionskörper,
00:14:16ein String-Literal,
00:14:17jede Art von Implementierungsdetail.
00:14:20Es wird diese Funktion invalidieren und dann werden wir dieselbe Menge an Imports berechnen.
00:14:25Und dann invalidieren wir nichts, was dies gelesen hat.
00:14:29Wenn Sie also darüber in einer HMR-Sitzung nachdenken,
00:14:32bedeutet das,
00:14:32dass wir Ihre Datei neu parsen müssen,
00:14:34aber wir müssen uns wirklich nicht mehr Gedanken darüber machen,
00:14:38wie Chunking-Entscheidungen getroffen werden.
00:14:40Wir müssen uns keine Gedanken über Tree Shaking-Ergebnisse machen,
00:14:43weil wir wissen,
00:14:44dass sich diese nicht geändert haben.
00:14:45Wir können also sofort vom Parsen der Datei,
00:14:48dieser einfachen Analyse,
00:14:50direkt zur Erzeugung von Outputs springen.
00:14:53Und das ist eine der Möglichkeiten,
00:14:55wie wir wirklich schnelle Refresh-Zeiten haben.
00:14:57Das ist also ziemlich imperativ.
00:15:02Eine andere Möglichkeit,
00:15:03diese Grundidee zu betrachten,
00:15:05ist als Graph von Knoten.
00:15:06Hier auf der linken Seite könnte man sich einen Kalt-Build vorstellen.
00:15:12Anfangs müssen wir tatsächlich jede Datei lesen,
00:15:14sie alle parsen,
00:15:15alle Imports analysieren.
00:15:17Und als Nebeneffekt davon haben wir alle Abhängigkeitsinformationen aus Ihrer Anwendung gesammelt.
00:15:21Und wenn sich dann etwas ändert,
00:15:23können wir diesen aufgebauten Abhängigkeitsgraphen nutzen,
00:15:26um Invalidierungen zurück durch den Stack zu propagieren und Turbo Pack-Funktionen erneut auszuführen.
00:15:32Und wenn sie einen neuen Wert erzeugen, hören wir dort auf.
00:15:35Andernfalls propagieren wir die Invalidierung weiter.
00:15:37Also großartig.
00:15:41Wissen Sie,
00:15:41das ist eigentlich eine massive Vereinfachung dessen,
00:15:44was wir in der Praxis tun,
00:15:45wie Sie sich vorstellen können.
00:15:47In Turbo Pack gibt es heute etwa 2.500 verschiedene Turbo-Task-Funktionen.
00:15:53Und in einem typischen Build könnten wir buchstäblich Millionen verschiedener Aufgaben haben.
00:15:58Es sieht also vielleicht ein bisschen mehr so aus.
00:16:01Nun, ich erwarte nicht wirklich, dass Sie das lesen können.
00:16:04Konnte es nicht wirklich auf die Folie passen.
00:16:06Also vielleicht sollten wir herauszoomen.
00:16:08Okay, das ist offensichtlich nicht hilfreich.
00:16:14In Wirklichkeit haben wir bessere Wege,
00:16:17um zu verfolgen und zu visualisieren,
00:16:19was in Turbo Pack passiert.
00:16:21Aber im Grunde funktionieren diese,
00:16:22indem sie die überwiegende Mehrheit der Abhängigkeitsinformationen verwerfen.
00:16:26Und jetzt vermute ich,
00:16:27dass einige von Ihnen vielleicht tatsächlich Erfahrung mit Signals haben,
00:16:32vielleicht schlechte Erfahrungen.
00:16:34Wissen Sie,
00:16:35ich persönlich mag Stack-Traces und die Möglichkeit,
00:16:38in einem Debugger in und aus Funktionen zu springen.
00:16:41Vielleicht sind Sie also misstrauisch,
00:16:43dass dies das komplette Allheilmittel ist.
00:16:45Es kommt offensichtlich mit Kompromissen.
00:16:47Und ja,
00:16:48dazu würde ich natürlich sagen,
00:16:50nun,
00:16:51wissen Sie,
00:16:52was ich eigentlich sagen würde,
00:16:54ist,
00:16:55dass es in der gesamten Softwareentwicklung darum geht,
00:16:59Kompromisse zu managen.
00:17:01Wir lösen Probleme nicht immer exakt,
00:17:04sondern wählen wirklich neue Kompromiss-Sets,
00:17:07um Wert zu liefern.
00:17:08Um unsere Designziele bezüglich inkrementeller Builds in Turbo Pack zu erreichen,
00:17:13haben wir sozusagen alles auf dieses inkrementelle reaktive Programmiermodell gesetzt.
00:17:19Und das hatte natürlich einige sehr natürliche Konsequenzen.
00:17:23Also,
00:17:24wissen Sie,
00:17:25vielleicht haben wir das Problem von handgeschriebenen Caching-Systemen und umständlicher Invalidierungslogik tatsächlich gelöst.
00:17:33Im Gegenzug müssen wir eine komplizierte Caching-Infrastruktur verwalten.
00:17:39Und natürlich,
00:17:39wissen Sie,
00:17:40das klingt für mich nach einem wirklich guten Kompromiss.
00:17:42Ich mag komplizierte Caching-Infrastruktur,
00:17:44aber wir alle müssen mit den Konsequenzen leben.
00:17:48Das erste ist natürlich einfach der Kern-Overhead dieses Systems.
00:17:54Wissen Sie,
00:17:55wenn Sie es in einem bestimmten Build oder einer HMR-Sitzung betrachten,
00:18:01ändern Sie nicht wirklich sehr viel.
00:18:04Wir verfolgen also alle Abhängigkeitsinformationen zwischen jedem Import und jedem Resolve-Ergebnis in Ihrer Anwendung,
00:18:10aber Sie werden tatsächlich nur wenige davon ändern.
00:18:13Die meisten der von uns gesammelten Abhängigkeitsinformationen werden also nie tatsächlich benötigt.
00:18:16Wissen Sie,
00:18:17um dies zu bewältigen,
00:18:18mussten wir uns stark darauf konzentrieren,
00:18:21die Leistung dieser Caching-Schicht zu verbessern,
00:18:24um den Overhead zu reduzieren und unser System auf immer größere Anwendungen zu skalieren.
00:18:30Und das nächste und offensichtlichste ist einfach der Speicher.
00:18:34Wissen Sie,
00:18:35Caches sind immer grundsätzlich ein Kompromiss zwischen Zeit und Speicher.
00:18:38Und unser macht da eigentlich nichts anders.
00:18:41Unser einfaches Ziel ist,
00:18:43dass die Cache-Größe linear mit der Größe Ihrer Anwendung skalieren sollte.
00:18:49Aber auch hier müssen wir vorsichtig mit dem Overhead sein.
00:18:51Dieser nächste Punkt ist etwas subtil.
00:18:54Wir haben also viele Algorithmen im Bundler,
00:18:57wie Sie vielleicht erwarten.
00:18:58Und einige davon erfordern eine Art globales Verständnis Ihrer Anwendung.
00:19:03Nun,
00:19:03das ist ein Problem,
00:19:04denn jedes Mal,
00:19:05wenn Sie von globalen Informationen abhängen,
00:19:07bedeutet das,
00:19:08dass jede Änderung diese Operation invalidieren könnte.
00:19:10Wir müssen also vorsichtig sein,
00:19:12wie wir diese Algorithmen entwerfen,
00:19:14Dinge sorgfältig zusammensetzen,
00:19:15damit wir die Inkrementalität bewahren können.
00:19:17Und schließlich,
00:19:19das hier ist vielleicht ein persönlicher Kritikpunkt.
00:19:24Alles ist asynchron in Turbo Pack.
00:19:27Und das ist großartig für die horizontale Skalierbarkeit,
00:19:29aber wieder einmal schadet es unseren grundlegenden Zielen wie Debugging,
00:19:32Performance-Profiling.
00:19:38Ich bin sicher,
00:19:39viele von Ihnen haben Erfahrung mit dem Debuggen von Async in den Chrome Dev Tools.
00:19:46Und das ist im Allgemeinen eine ziemlich gute Erfahrung.
00:19:48Nicht immer ideal.
00:19:49Und ich versichere Ihnen,
00:19:51Rust mit LLDB ist Lichtjahre zurück.
00:19:53Um das zu bewältigen,
00:19:55mussten wir in benutzerdefinierte Visualisierungs-,
00:19:58Instrumentierungs- und Tracing-Tools investieren.
00:20:01Und sehen Sie mal,
00:20:02wieder ein Infrastrukturprojekt,
00:20:04das kein Bundler ist.
00:20:07Okay,
00:20:07schauen wir mal,
00:20:08ob wir die richtige Wette eingegangen sind.
00:20:11Bei Vercel haben wir also eine sehr große Produktionsanwendung.
00:20:17Wir denken,
00:20:17es ist vielleicht eine der größten der Welt,
00:20:19aber,
00:20:19wissen Sie,
00:20:20wir wissen es nicht wirklich.
00:20:21Aber es hat etwa 80.000 Module.
00:20:23Schauen wir uns also an, wie Turbo Pack damit umgeht.
00:20:26Für Fast Refresh dominieren wir wirklich das,
00:20:30was Web Pack liefern kann.
00:20:32Aber das sind alte Nachrichten.
00:20:33Turbo Pack für die Entwicklung ist schon eine Weile draußen,
00:20:36und ich hoffe wirklich,
00:20:37dass es jeder zumindest in der Entwicklung verwendet.
00:20:39Aber,
00:20:39wissen Sie,
00:20:40das Neue hier heute ist natürlich,
00:20:41dass Builds stabil sind.
00:20:42Schauen wir uns also einen Build an.
00:20:44Und hier sehen Sie einen erheblichen Gewinn gegenüber Web Pack für diese Anwendung.
00:20:49Dieser spezielle Build läuft tatsächlich mit unserer neuen experimentellen Dateisystem-Caching-Schicht.
00:20:53Etwa 16 dieser 94 Sekunden sind nur das Leeren des Caches am Ende.
00:20:59Und daran werden wir arbeiten,
00:21:00um es zu verbessern,
00:21:01wenn das Dateisystem-Caching stabil wird.
00:21:04Aber natürlich ist die Sache mit Kalt-Builds,
00:21:05dass sie kalt sind,
00:21:06nichts ist inkrementell.
00:21:07Schauen wir uns also einen echten Warm-Build an.
00:21:10Mit dem Cache vom Kalt-Build können wir das sehen.
00:21:14Das ist also nur ein kleiner Einblick, wo wir heute stehen.
00:21:17Da wir dieses feingranulare Caching-System haben,
00:21:19können wir den Cache tatsächlich einfach auf die Festplatte schreiben und dann beim nächsten Build wieder einlesen,
00:21:23herausfinden,
00:21:24was sich geändert hat,
00:21:25und den Build abschließen.
00:21:26Okay,
00:21:27das sieht ziemlich gut aus,
00:21:28aber viele von Ihnen denken vielleicht: Nun,
00:21:30ich persönlich habe vielleicht nicht die größte Next.js-Anwendung der Welt.
00:21:34Schauen wir uns also ein kleineres Beispiel an.
00:21:37Die react.dev-Website ist ein gutes Stück kleiner.
00:21:41Es ist auch irgendwie interessant,
00:21:42weil es ein React-Compiler ist.
00:21:44Es ist wenig überraschend ein früher Anwender des React-Compilers.
00:21:47Und der React-Compiler ist in Babel implementiert.
00:21:49Und das ist eine Art Problem für unseren Ansatz,
00:21:51denn es bedeutet,
00:21:52dass wir für jede Datei in der Anwendung Babel bitten müssen,
00:21:54sie zu verarbeiten.
00:21:55Also,
00:21:55und grundsätzlich würde ich sagen,
00:21:57wir,
00:21:58oder ich,
00:21:58ich kann den React-Compiler nicht schneller machen.
00:22:01Das ist nicht meine Aufgabe.
00:22:02Meine Aufgabe ist Turbo Pack.
00:22:03Aber wir können genau herausfinden,
00:22:05wann wir es aufrufen müssen.
00:22:07Wenn man sich also die Fast Refresh-Zeiten ansieht,
00:22:10war ich tatsächlich ein wenig enttäuscht von diesem Ergebnis.
00:22:13Und es stellt sich heraus,
00:22:15dass etwa 130 dieser 140 Millisekunden der React-Compiler sind.
00:22:18Und sowohl Turbo Pack als auch Web Pack tun das.
00:22:22Aber mit Turbo Pack können wir,
00:22:23nachdem der React-Compiler diese Änderung verarbeitet hat,
00:22:26sehen: Oh,
00:22:27Imports haben sich nicht geändert.
00:22:29Werfen Sie es in den Output und machen Sie weiter.
00:22:31Wieder einmal sehen wir bei Kalt-Builds diesen konsistenten 3-fachen Gewinn.
00:22:37Und nur um das klarzustellen, das ist auf meinem Rechner.
00:22:39Aber wieder, keine Inkrementalität in einem Kalt-Build.
00:22:44Und in einem Warm-Build sehen wir diese viel bessere Zeit.
00:22:47Also wieder,
00:22:47bei einem Warm-Build haben wir den Cache bereits auf der Festplatte.
00:22:52Alles,
00:22:52was wir tun müssen,
00:22:53ist im Grunde,
00:22:54sobald wir starten,
00:22:55herauszufinden,
00:22:55welche Dateien in der Anwendung sich ändern,
00:22:57diese Jobs erneut auszuführen und dann alles andere vom vorherigen Build wiederzuverwenden.
00:23:01Die grundlegende Frage ist also: Sind wir schon Turbo?
00:23:05Ja.
00:23:06Also ja, das wurde natürlich in der Keynote besprochen.
00:23:09Turbo Pack ist ab Next 16 stabil.
00:23:12Und wir sind sogar der Standard-Bundler für Next.
00:23:14Also, wissen Sie, Mission erfüllt, gern geschehen.
00:23:17Aber. (lacht) (Publikum applaudiert)
00:23:23Und wenn Sie diese Revert-Sache in der Keynote bemerkt haben,
00:23:26das war ich,
00:23:27als ich versucht habe,
00:23:28Turbo Pack zum Standard zu machen.
00:23:30Es hat nur drei Versuche gebraucht.
00:23:31Aber was ich Ihnen wirklich noch einmal mitgeben möchte,
00:23:35ist das hier.
00:23:35Wissen Sie, denn wir sind noch nicht fertig.
00:23:37Wir haben noch viel in Bezug auf die Leistung zu tun und müssen die Dateisystem-Caching-Schicht fertigstellen.
00:23:42Ich schlage vor,
00:23:43Sie alle probieren es in der Entwicklung aus.
00:23:44Und das war's.
00:23:46Vielen Dank.
00:23:47Bitte sprechen Sie mich an, stellen Sie mir Fragen.
00:23:49(Publikum applaudiert) (fröhliche Musik) (fröhliche Musik)

Key Takeaway

Turbo Pack revolutioniert die Bundling-Leistung durch ein inkrementelles, reaktives Caching-System, das automatische Abhängigkeitsverfolgung und selektive Neuberechnung nutzt, um Entwicklern unabhängig von der Anwendungsgröße schnelle Build- und Refresh-Zeiten zu bieten.

Highlights

Turbo Pack nutzt ein inkrementelles, reaktives Programmiermodell, inspiriert von "Signals", um die Bundling-Leistung zu optimieren.

Das System macht jede teure Operation cachebar und verfolgt Abhängigkeiten automatisch, um manuelle Cache-Invalidierungen zu vermeiden.

"TurboTask"-Funktionen und "Value Cells" in Rust ermöglichen feingranulares Caching und selektive Neuberechnungen bei Änderungen.

Bei Dateiänderungen werden nur die direkt betroffenen "TurboTask"-Funktionen neu ausgeführt; wenn die Ergebnisse (z.B. Imports) gleich bleiben, stoppt die Invalidierungskette.

Turbo Pack zeigt signifikante Leistungsverbesserungen gegenüber Web Pack, insbesondere bei Kalt- und Warm-Builds sowie Fast Refresh, selbst bei großen Anwendungen.

Das Dateisystem-Caching ermöglicht die Wiederverwendung von Build-Caches über Sitzungen hinweg, was zu extrem schnellen Warm-Build-Zeiten führt.

Trotz Vorteilen gibt es Kompromisse wie den Overhead des Systems, Speicherverbrauch und die Komplexität des Debuggings asynchroner Rust-Code, die durch spezielle Tools gemanagt werden.

Timeline

Einführung in Turbo Pack und die Vision

Luke Sandberg, Software-Ingenieur bei Vercel, stellt sich und seine Arbeit an Turbo Pack vor. Er kam von Google und war überrascht von den Zielen des Vercel-Teams. Der Vortrag wird die Designentscheidungen von Turbo Pack beleuchten, die auf der bereits bestehenden Leistung aufbauen sollen. Das übergeordnete Designziel deutet auf die Notwendigkeit schwieriger Entscheidungen hin, um die angestrebte Performance zu erreichen.

Die Kernidee der Inkrementalität und Caching

Der Sprecher betont, dass Kalt-Builds zwar wichtig sind, aber idealerweise gar nicht erst erlebt werden sollten. Die zentrale Idee zur Verbesserung der Bundling-Leistung ist die Inkrementalität, die durch umfassendes Caching erreicht wird. Ziel ist es, jede Operation im Bundler cachebar zu machen, sodass bei Änderungen nur die direkt betroffene Arbeit wiederholt werden muss. Die Kosten eines Builds sollen mit der Größe der Änderung skalieren, nicht mit der Komplexität der gesamten Anwendung, um konstante Leistung zu gewährleisten. Ein naives "Baby-Bundler"-Beispiel zeigt die Probleme redundanter Arbeit ohne Inkrementalität, wie mehrfaches Parsen und Auflösen von Imports.

Herausforderungen und Fallstricke des naiven Caching

Zunächst wird ein einfacher Cache für die Parse-Funktion vorgestellt, der jedoch schnell an seine Grenzen stößt. Probleme wie Dateiänderungen, Symlinks, unterschiedliche Build-Ziele (Client/Server) und die Mutation von ASTs machen einen einfachen Cache unzureichend. Eine erweiterte Version, die Transforms und Konfiguration in den Cache-Schlüssel einbezieht, wird als zu komplex und fehleranfällig für Entwickler kritisiert. Die manuelle Invalidierungslogik über Callbacks führt zu einem "reaktiven Bundler", der aber immer noch das gesamte Bundle neu ausführt, anstatt nur inkrementelle Änderungen zu verarbeiten. Das manuelle Verwalten von Caches für Imports, Resolve-Ergebnisse und Output-Generierung mit Callbacks wird als unübersichtlich und unpflegbar dargestellt.

Anforderungen an ein robustes inkrementelles System

Der Sprecher leitet aus den Problemen des naiven Ansatzes konkrete Anforderungen an ein robustes Bundler-System ab. Es soll jede teure Operation einfach cachebar machen und die manuelle Verfolgung von Inputs oder Abhängigkeiten durch Entwickler überflüssig machen. Das System muss automatisch korrekte Cache-Schlüssel generieren und narrensicher sein. Weiterhin muss es reaktiv auf sich ändernde Inputs wie Dateien, Konfigurationen oder Umgebungsvariablen reagieren können, ohne dass Callbacks manuell durchgereicht werden müssen. Schließlich soll das System moderne Architekturen nutzen, um Multi-Threading und hohe Geschwindigkeit zu gewährleisten.

Turbo Pack's Lösung: Das Signals-Modell und TurboTask-Funktionen

Die vorgestellten Anforderungen führen zu einem System, das dem Konzept von "Signals" ähnelt, inspiriert vom Rust-Compiler und Systemen wie Salsa (Adaptons). Turbo Pack implementiert dies mit "TurboTask"-Funktionen, die als gecachte Arbeitseinheiten dienen und automatisch Abhängigkeiten verfolgen. "VC" (Value Cell)-Typen repräsentieren reaktive Werte; wenn sie gelesen werden, wird eine Abhängigkeit registriert. Bei einer Änderung wird das Ergebnis der "TurboTask"-Funktion mit dem vorherigen verglichen, und nur bei einer tatsächlichen Wertänderung werden nachfolgende Abhängigkeiten invalidiert und neu berechnet. Dies ermöglicht beispielsweise, dass bei einer Moduländerung, die die Imports nicht beeinflusst, nachfolgende Schritte wie Chunking oder Tree Shaking nicht erneut ausgeführt werden müssen, was zu sehr schnellen Refresh-Zeiten führt.

Abhängigkeitsgraphen und die Kompromisse des Designs

Das System wird als ein Graph von Knoten beschrieben, bei dem ein Kalt-Build den vollständigen Abhängigkeitsgraphen erstellt und ein Warm-Build diesen nutzt, um Invalidierungen effizient zu propagieren. In der Praxis umfasst Turbo Pack heute etwa 2.500 "TurboTask"-Funktionen und Millionen von Aufgaben in einem typischen Build. Der Sprecher geht auf die Kompromisse dieses Ansatzes ein, darunter der Overhead des Systems durch die Verfolgung vieler ungenutzter Abhängigkeiten und der erhöhte Speicherverbrauch. Algorithmen, die globale Informationen benötigen, müssen sorgfältig entworfen werden, um die Inkrementalität zu bewahren. Zudem erschwert die Asynchronität des Rust-Codes das Debugging, was Investitionen in benutzerdefinierte Visualisierungs- und Tracing-Tools erforderlich macht.

Leistungsbewertung und Ausblick von Turbo Pack

Die Leistungsfähigkeit von Turbo Pack wird anhand von Vercels großer Produktionsanwendung (ca. 80.000 Module) demonstriert. Turbo Pack übertrifft Web Pack deutlich bei Fast Refresh-Zeiten und zeigt einen dreifachen Gewinn bei Kalt-Builds (94s vs. 290s), wobei das experimentelle Dateisystem-Caching noch optimiert wird. Besonders beeindruckend sind die Warm-Build-Zeiten von nur 2 Sekunden, da der Cache von der Festplatte wiederverwendet wird. Auch bei kleineren Anwendungen wie der react.dev-Website, die den React-Compiler nutzt, zeigt Turbo Pack Vorteile, indem es nach der Compiler-Verarbeitung schnell zum Output springt, wenn sich Imports nicht geändert haben. Der Sprecher bestätigt, dass Turbo Pack ab Next 16 stabil und der Standard-Bundler ist, betont aber, dass die Entwicklung noch nicht abgeschlossen ist und weitere Leistungsverbesserungen sowie die Finalisierung des Dateisystem-Cachings anstehen.

Community Posts

View all posts