Wir haben Slash im Rahmen unserer Finanzierungsrunde der Serie Bund wir wollten unser Produkt zusammen mit unserer Marke optisch auffrischen. Wir haben schnell erkannt, dass dies neben der Aktualisierung unserer Benutzeroberfläche eine großartige Gelegenheit war, einen Großteil der technischen Schulden anzugehen, die wir in den letzten drei Jahren aufgrund unseres extrem schnellen Wachstums angehäuft hatten.
Somit Projekt Gesichtsstraffung wurde geboren – eine Chance für uns, den Grundstein für Slashs Frontend für unser wachsendes Unternehmen und Team zu legen. Wir wollten Entscheidungen treffen, die den Test der Zeit bestehen würden, indem wir aus den Fehlern der Vergangenheit lernten.
Teil 1: Umgang mit technischen Schulden
In diesem Abschnitt werde ich auf jeden Punkt zum Thema „Schulden” eingehen. Später in Teil 2 werde ich näher auf unsere Lösungen für jeden Punkt eingehen.
Emotion: CSS in JS-Laufzeitengpässe
Unser ursprüngliches Designsystem, das auf Emotion und hat uns gute Dienste geleistet. Der Hauptvorteil einer CSS-in-JS-Bibliothek war die Zusammenführung von Markup und Styling einer Komponente, was die Iteration extrem beschleunigte. Als unsere Kunden jedoch wuchsen und immer größere Datenmengen hatten, stellten wir Leistungsengpässe bei Emotion fest. CSS-in-JS erfordert Laufzeit-Overhead und kann die Haupt-Event-Schleife verlangsamen. Dies macht sich bemerkbar, wenn große Listen virtualisiert und schnell gescrollt werden. Wenn Komponenten schnell montiert und demontiert werden, beeinträchtigt die dynamische Berechnung von Stilen und die Generierung von Stylesheets den Browser so stark, dass er nicht mehr mit 60 fps rendern kann. Andere Unternehmen haben ähnliche Leistungsengpässe festgestellt und haben uns von Lösungen entfernt, die von Laufzeit-Overhead abhängig sind.
Prop-basierte „Gott-Komponenten“
Das ursprüngliche Konzept von „Gott widerspricht” stammt aus der objektorientierten Programmierung (OOP), wo ein Objekt allein zu viel Verantwortung tragen würde. Wir haben ähnliche Dinge in unseren React-Komponenten beobachtet, die wir als „God Components” bezeichnet haben.
Viele unserer React-Originalkomponenten wurden mit einer props-basierten Schnittstelle geschrieben, bei der Sie Daten und Callbacks bereitstellen und eine einzelne Komponente rendern. Oberflächlich betrachtet bietet dies eine elegante Schnittstelle: Sie stellen einfach Daten und Props bereit und vertrauen darauf, dass die Komponente sich um alle Markups und die Geschäftslogik kümmert.
In der Praxis werden diese Komponenten mit zunehmender Größe des Codes erweitert, um Anwendungsfälle abzudecken, die ursprünglich nicht berücksichtigt oder vorgesehen waren. Jedes Mal, wenn ein Entwickler eine neue Konfiguration benötigte, fügte er Props hinzu, was die Wartung und Nutzung mit der Zeit erschwerte. Diese Komponenten wurden schließlich zu „God Components“, die sorgfältig gewartet werden mussten, mit einmaligen Props zur Lösung bestimmter Probleme oder „Escape Hatches“, um zusätzliche Markups in die Komponente selbst einzufügen/zu ersetzen (ähnlich einem Render-Prop-Muster). Ich werde später in diesem Blog Code-Beispiele zeigen.
Inkonsistente Formularverwaltung
Unsere ursprüngliche Lösung für das Formularmanagement war einfach React-Statusverwaltung. Der lokale Status ist einfach zu verwenden, aber es mangelt ihm an Standardisierung und tiefer Typsicherheit, was zu einer inkonsistenten Codequalität führte. Die Art und Weise, wie jeder Ingenieur den lokalen Status implementierte, war etwas unterschiedlich – einige nutzten die Kontext-API mit einem großen gemeinsamen Formularstatus, während andere die Felder in einzelne useState Anrufe.
Wenn es darum ging, diese Formulare zu erweitern, wurden häufig Metadaten wie istFeldberührt würde hinzugefügt werden, wodurch eine unübersichtliche Komponente mit vielen explizit definierten lokalen Zuständen entstehen würde, um Formularinteraktionen zu verarbeiten, die standardisiert werden sollten.
Teil 2: Ausführung
Nachdem wir nun die aktuellen Schwachstellen unserer Codebasis aufgezeigt haben, werde ich mich eingehend mit den Lösungen und den dahinterstehenden Entscheidungen befassen.
Umstellung auf Tailwind v4.0
Bei der Bewertung von Styling-Lösungen hatten wir zwei Finalisten: Tailwind v4.0 und PandaCSS, da beide keinen Laufzeit-Overhead haben und die gemeinsame Speicherung von Markup und Styling einer Komponente ermöglichen. Die Vorteile von PandaCSS waren Typsicherheit und eine geringe Lernkurve, da unser bestehendes Designsystem sehr ähnlich aussah.
Die Vorteile von Tailwind waren ein ausgereiftes Ökosystem und die Vertrautheit der Entwickler. Viele unserer Ingenieure hatten Tailwind für persönliche Projekte verwendet und waren davon sehr angetan, mich eingeschlossen. Es gibt fantastische Tools mit integrierter Tailwind-Unterstützung, wie zum Beispiel Supernova und Tailwind-VariantenKI-Codierungstools eignen sich auch hervorragend zum Schreiben von Tailwind-Klassennamen, wodurch die Migration wesentlich vereinfacht wird.
Um die Leistungsvorteile ohne Laufzeit-Overhead zu veranschaulichen, finden Sie hier die Vorher-Nachher-Flame-Charts derselben Operation (schnelles Scrollen in einer großen, virtualisierten Tabellenansicht) in Emotion und Tailwind:


Figma-zu-Code-Pipeline
Bei der Migration zu Tailwind wollten wir eine Möglichkeit finden, unsere Figma-Token und unseren Code synchron zu halten, wobei Figma unsere Quelle der Wahrheit ist. Unser Designsystem in Figma wird von unserer Designagentur gepflegt. Metakarbon, der die Designentscheidungen trifft und das Stylesheet mit semantischen Tokens erstellt.
Unser Ziel war es, eine Pipeline zu schaffen, in der alle Änderungen am zugrunde liegenden Designsystem manuell überprüft und mit minimalem Aufwand implementiert werden können. Wir haben uns für Supernova entschieden, um Design-Token aus unserem Figma in den Code zu übertragen, da sie über einen hervorragenden Tailwind v4.0-Code-Extractor verfügen.
Bei der Arbeit mit unserer CSS-Ausgabe stießen wir auf ein interessantes Problem: Wir wollten, dass semantische Token auf unterschiedliche Werte verweisen, je nachdem, auf welche Eigenschaft sie angewendet wurden. Hier ein anschauliches Beispiel:
Wir wollen neutral-dezent-Standard in 3 verschiedene Farbtöne aufzulösen lichtneutrales Produkt: 100, 500 und 1000 - Wenn wir jedoch nur dies verwenden würden, würde Tailwind v4.0 --Farbe Themenvariable würde spawnen bg-bg-neutral-subtil-Standard, Rahmenhintergrund-neutral-dezent-Standard, bg-text-neutral-subtil-Standard und viele andere Klassennamen, die sowohl unsinnig als auch redundant in der Benennung sind.
Daher haben wir ein benutzerdefiniertes Build-Skript geschrieben, um unsere Ausgabe so umzuwandeln, dass sie die spezifischeren Funktionen von Tailwind v4.0 nutzt. Themenvariablennamensraum:
Was zu classNames wie bg-neutral-subtil-Standard, Grenznachbar-neutral-subtil-Standard und Text-neutral-subtil-Standard - die sich alle in ihren unterschiedlichen Schattierungen auflösen.
Wenn sich also unser zugrunde liegendes Designsystem ändert, müssen wir lediglich unseren Supernova-Exporter ausführen und die Ausgabe zur Überprüfung in unsere Codebasis einfügen. Anschließend konvertiert unser Build-Skript die Rohausgabe automatisch in eine erstellte CSS-Datei, die von allen unseren Paketen verwendet wird.
At Slash, we follow a philosophy of maintaining control over our tooling . This allows us to extend functionality quickly and prevent vendor lock in. It also helps us diagnose and fix issues directly. By having our build script, we aren’t locked into Supernova as a vendor, and can extend it - like adding support for divide to be the same color as border, or light vs dark mode themes.
Other examples include our query builder and JSON schema to typescript code generation pipeline.
Headless-UI-Bibliotheken: Base UI und React Aria
Zunächst möchte ich erklären, warum wir eine Headless-UI-Bibliothek und keine vorgefertigte Lösung wollten. Der Sinn der Neugestaltung besteht darin, unsere Markenidentität durch unser Produkt klar zum Ausdruck zu bringen – und die Verwendung einer vorgefertigten Lösung wie Shadcn oder MaterialUI würde diesem Zweck zuwiderlaufen, da unsere Komponenten dadurch vorgefertigt würden.
Vor dem Facelift haben wir Radix UI verwendet, aber wir waren nicht sicher über seine Zukunft, als wir sahen, dass die Kernentwickler zu Base UI wechselten. Base UI hat sich zu einer einfachen und erweiterbaren Bibliothek entwickelt, die unseren Anforderungen sehr gut entspricht und eine hervorragende Barrierefreiheit bietet. Angesichts seiner hervorragenden Entwickler gibt uns Base UI Zuversicht für die Zukunft, dass es mit den Anforderungen von Slash mitwachsen wird.
Für bestimmte Komponenten, die noch nicht von Base UI unterstützt werden (z. B. unser Datumswähler), haben wir uns für React Aria entschieden, das bei unserer Suche nach einer Headless-UI den zweiten Platz belegt hat. React Aria ist praxiserprobt und hält sich strikt an alle Barrierefreiheitsstandards. Letztendlich haben wir unsere Komponenten auf Base UI aufgebaut, da der Ansatz von React Aria recht eigenwillig ist und erfordert, dass wir uns vollständig auf das React Aria-Ökosystem einlassen, und wir etwas mit weniger mentalem Aufwand bevorzugten.
Verbundkomponenten
Wie bereits erwähnt, sind props-basierte Komponenten zunächst elegant, leiden jedoch unter einer schlechten Erweiterbarkeit. Unsere Lösung für die Erstellung besser erweiterbarer Komponenten bestand darin, uns stark auf die Verbundkomponentenarchitektur, das die Markup-Kontrolle einer Komponente an ihr übergeordnetes Element übergibt. Der Unterschied zwischen den beiden lässt sich am besten anhand eines Beispiels veranschaulichen – hier ist eine vereinfachte Version unserer alten und neuen durchsuchbaren Auswahlkomponente:
Und hier ist unsere neue durchsuchbare Auswahlkomponente, die eine zusammengesetzte Komponentenarchitektur verwendet:
Auf den ersten Blick sind die zusammengesetzten Komponenten umständlicher – der Entwickler muss das Markup-Muster kennen und befolgen. Der Vorteil liegt in der Flexibilität bei der Erweiterung des Markups, da die vollständige Kontrolle beim übergeordneten Element liegt. Daher gelangen Markup-Hacks niemals in die Kernkomponente. Wenn ein Entwickler eine Fußzeile zum Auswahlfeld hinzufügen muss, fügt er sie einfach zum Markup hinzu – ohne dass ein Fußzeile auswählen Stütze für die Kernkomponente! Wenn eine Fußzeile zu einer allgemeinen Anforderung wird, erstellen Sie eine <SearchableSelect.Footer /> Komponente!
Eine weitere häufige Gefahr bei zusammengesetzten Komponenten besteht darin, dass Ingenieure Implementierungen gängiger Muster duplizieren können, die nicht in der Kernkomponente enthalten sind. Daher muss unser Team gängige Muster besser kennen und sie bei Bedarf zur Kernkomponente hinzufügen. Sie können zusammengesetzte Komponenten auch als zugrunde liegende API für eine Einzeiler-Komponente verwenden – wir tun dies jedoch sehr bewusst, um diese Komponenten nicht zu „Gottkomponenten” zu machen. In einem späteren Abschnitt gehe ich näher darauf ein, wie wir den Wissensaustausch über Storybook angehen.
Tailwind-Varianten
Bei der tatsächlichen Gestaltung unserer Komponenten haben wir uns stark auf Tailwind-VariantenEinige von uns haben Klassenabweichungsbehörde (CVA) und wollten diese Art von Variantenlogik in unserer Styling-Lösung. Tailwind Variants hatte eine einige Funktionen, die uns dazu bewogen haben, es zu verwenden, wobei die Slots-API und die integrierte Konfliktlösung die wichtigsten sind.
Letztendlich brauchten wir nur eine Lösung, die unseren Styling-Varianten-Code übersichtlich hielt und erweiterbar genug war, um unseren Ansatz mit zusammengesetzten Komponenten abzudecken. Durch die Verwendung von Tailwind Variants konnten wir unseren alten bedingten Styling-Code erheblich standardisieren, der je nach den Eigenschaften des div-Elements mit Emotion-Styling explizit unterschiedliche Tokens anwendete.
Nachfolgend finden Sie ein anschauliches Beispiel für die Implementierung und Verwendung unserer Schaltflächen:
Wie Sie sehen können, ist der Aufrufer von Knopf muss eigentlich nicht verwendet werden Schaltflächenvarianten - es übergibt Requisiten an Knopf als Konfigurationsoptionen, die dann zum Aufruf verwendet werden Schaltflächenvarianten intern. Wir nutzen auch die ausdehnen Funktion zum Teilen von Stilen zwischen ähnlichen Komponenten, wie Eingabefeldern und Auswahlfeldern.
Sie werden auch feststellen, wie einfach es ist, die Stile zu scannen, dank unserer semantischen Klassennamen.
Testen und Dokumentation: Storybook + Chromatic
In der ersten Hälfte des Projekts arbeitete ich alleine und schätzte Tests und Dokumentation nicht annähernd so sehr, wie ich es hätte tun sollen. Als ich jedoch die Basis-Komponentenbibliothek fertiggestellt hatte und weitere Ingenieure hinzukamen, um ihre jeweiligen Produkte zu überarbeiten, wurde immer deutlicher, dass wir einen zentralen Ort brauchten, um ganz einfache Fragen wie „Haben wir diese Komponente?“ und „Wie verwende ich diese Komponente richtig?“ zu beantworten.
Einer unserer neuen Ingenieure, Sam, hat eigenhändig unsere Storybook- und Chromatic-Testsuite eingerichtet. Ohne diese hätten wir sehr schnell in die gleichen Fallen der technischen Verschuldung durch den Missbrauch von Komponenten tappen können – insbesondere angesichts der Lernkurve bei zusammengesetzten Komponenten.
Mit Storybook können wir Anwendungsbeispiele für alle Komponenten zentralisieren, sodass diese Fragen anhand von Code-Beispielen vollständig beantwortet werden können. Außerdem wird sofort ersichtlich, welche Komponente ein Entwickler verwenden sollte und ob sie die benötigten Konfigurationsoptionen unterstützt.
Chromatic erkennt visuelle Regressionen, sodass alle Änderungen an den zugrunde liegenden Komponenten sofort gemeldet werden, wenn sie sich auf die Darstellung auswirken – wodurch das Zusammenführen blockiert wird, bis sie manuell überprüft wurden.
Formularverwaltung: Zod vs. ArkType
Die Codebasis von Slash ist vollständig in Typescript geschrieben, und wir nutzen diesen Umstand intensiv, indem wir beim Erstellen unserer Codebasis gemeinsame Typen generieren. Wir wollten eine Lösung für die Formularverwaltung, die es Entwicklern erschwert, sich selbst zu schaden, und die unsere umfassende Typsicherheit nutzt.
Wir haben uns schließlich für ArkType entschieden, da es so eng mit Typescript verbunden war, dass wir jegliche Abweichungen verhindern konnten. Wir konnten unsere generierten Typen nehmen und sie eins zu eins mit ArkType-Typen koppeln, indem wir befriedigt, sodass bei einer Änderung unseres zugrunde liegenden Typs unsere Codebasis Typfehler ausgibt.
Hier ist ein anschauliches Beispiel:
Zwar hätten uns Zod und React Hook Form gute Dienste geleistet, doch wir wollten eine Lösung, die direkt die Vorteile von TypeScript nutzt, und hier war ArkType klar führend. Wir haben uns aufgrund der strengeren Einhaltung von TypeScript für Tanstack Form anstelle von React Hook Form entschieden.
Eine Sache, die Ihnen bei Tanstack Form auffallen wird, ist, dass es Sie zu einer strengen, eigenwilligen Formularstruktur zwingt. Das bedeutet zwar einen gewissen Lernaufwand, aber wir waren der Meinung, dass es sich lohnt, eine Standardisierung bei der Erstellung von Formularen in unserer gesamten Codebasis durchzusetzen.
Implementierung: Feature-Flag und Code-Verzweigung
Als es darum ging, den Code für das Facelift zu schreiben, wollten wir einen einzigen riesigen PR vermeiden, da wir neben dem Facelift weiterhin neue Funktionen im alten Designsystem auslieferten.
Wir haben ein Feature-Flagund einfach das Feature-Flag für nur unser Konto, wodurch wir Hundefutter unsere eigenen Änderungen. Wir haben auch eine einfache Symbolleiste implementiert, mit der sich das Facelift ein- und ausschalten lässt, sodass Regressionen durch schnelles Ein- und Ausschalten des Facelifts extrem leicht zu erkennen sind.

Was die eigentliche Codeverzweigung angeht, haben wir darauf geachtet, den Code für die Geschäftslogik nicht zu duplizieren, und sind dabei wie folgt vorgegangen:
Das obige Beispiel veranschaulicht einen Idealfall. - Die Realität der Einführung großer Mengen neuen Codes in eine bestehende Codebasis ist viel chaotischer. Beispiel: Wenn Sie die istFacelift Wenn Sie eine hohe Ebene (z. B. auf der obersten Seiteebene) umschalten, müssen Sie zwangsläufig die gesamte Geschäftslogik für alle Zweige unterhalb dieser Komponente duplizieren. Wenn Sie die Überprüfung jedoch auf der Ebene der einzelnen Komponenten durchführen, sind Sie an die alte Markup-Logik des übergeordneten Elements gebunden, die ohnehin oft einer Überarbeitung bedarf.
Was uns vor Produktionsproblemen bewahrt hat, war, all diese Funktionen hinter einem Feature-Flag zu verstecken, sodass für alle tatsächlichen Kunden istFacelift war immer falsch bis wir bereit waren. Intern ließen wir es eingeschaltet, damit wir Regressionen erkennen konnten, sobald sie in die Plattform gelangten.
Als das Facelift fertiggestellt war, haben wir uns mit Kunden zusammengetan, zu denen wir enge Beziehungen hatten, und ihnen die Möglichkeit gegeben, es zu deaktivieren, um Feedback zu erhalten und Regressionen zu erkennen, ohne bestehende Arbeitsabläufe zu blockieren.
Hinweise zur Ausführung
Obwohl der obige Abschnitt sehr übersichtlich gegliedert ist, entsprach dies keineswegs der Reihenfolge der Umsetzung. In Wirklichkeit wurden diese Entscheidungen durch Ausprobieren und schnelles Iterieren getroffen. Zum Beispiel:
- Mein erster Entwurf unseres CSS-Build-Skripts hat die
@utiltyRichtlinie, um jeden gewünschten benutzerdefinierten Klassennamen zu erstellen – darauf baute Sam auf, indem er Folgendes fand GitHub-Diskussion in dem das Thema „Tailwind“ und variable Namespaces behandelt wurden. - Es gab zwei Iterationen unserer durchsuchbaren Einzelauswahlkomponente, die verschiedene BaseUI-Komponenten verwendeten, bevor wir uns für unseren aktuellen Ansatz entschieden haben, bei dem wir die Menü-API.
All dies bedeutet, dass wir diese Entscheidungen nicht so elegant getroffen haben, wie es die oben beschriebenen Kompromisse vermuten lassen. Der rote Faden ist die ständige Iteration, ohne sich durch das Gefühl lähmen zu lassen, dass man es beim ersten Mal perfekt machen muss.
Teil 3: Erkenntnisse
Ich persönlich habe viel aus einem Projekt gelernt, dessen Umfang das gesamte Frontend umfasste. Auch unser Team hat viel gelernt, insbesondere da immer mehr Ingenieure an der Neugestaltung ihrer jeweiligen Produkte mitgewirkt haben.
„Die Messlatte höher legen“
Ein Hauptgrund für die Entstehung von technischen Schulden war zunächst der Mangel an gemeinsamem Wissen. In einem jungen Start-up ist dies kein Problem, da alle eng zusammenarbeiten. Wenn ein Team jedoch wächst und die Produktpalette erweitert wird, ist es nicht mehr möglich, Annahmen und Nutzungsmuster so schnell wie zuvor auszutauschen.
Unsere Philosophie für das Facelifting war es, „die Messlatte höher zu legen“. Wir möchten es schwierig machen, schlechten Frontend-Code zu schreiben. Das bedeutet, dass wir dokumentieren, wie guter Frontend-Code aussieht, und Fehler erkennen, bevor sie in die Produktion gelangen. Storybook und Chromatic sind dafür unverzichtbar und die Investition in ihre Einrichtung lohnt sich. Jeder Aufwand, den Sie betreiben, um die Messlatte höher zu legen, wird sich positiv auf die Codequalität in der gesamten Codebasis auswirken – und umgekehrt.
Realität langer Projekte
Das Facelifting dauerte von Anfang bis Ende etwa vier Monate ununterbrochener Arbeit, mit einer speziellen „Hack Week“ am Ende, die drei Wochen dauerte und an der sechs weitere Ingenieure beteiligt waren (vielen Dank an sie <3). Ich hatte auch einen hervorragenden Projektmanager (Andy), der mir dabei half, das gesamte Projekt zu planen, mit Metacarbon zu kommunizieren und Feedback zu neuen UX-Mustern zu geben.
Da das Facelifting kein Projekt war, das ich in Teilen veröffentlichen konnte, hatte ich lange Zeit nicht das Vergnügen, Produkte für Kunden zu veröffentlichen. Ich habe gelernt, dass es sinnvoll ist, intern den Kontext zu wechseln – manchmal arbeitete ich an der Kernkomponentenbibliothek, dann wandte ich diese Kernkomponenten auf einige vollständige Seiten an und wechselte zwischen beiden hin und her. Dieser Ansatz hilft Ihnen auch dabei, Randfälle bei der Verwendung Ihrer Kernkomponenten schnell zu erkennen, sodass Sie gezwungen sind, zurückzugehen und bessere Abstraktionslinien zu ziehen.
Es kann demoralisierend sein, Unmengen an Code zu überarbeiten, ohne dass man dafür die Anerkennung der Kunden erhält. Was mich motiviert hat, waren mehrere Dinge:
- Dank der Leute bei Slash machte es mir immer noch Spaß, jeden Tag zur Arbeit zu kommen!
- Ich konnte den Fortschritt immer noch intern präsentieren. Wenn man anderen den eigenen Fortschritt zeigen kann, verspürt man eine Art Dopamin-Kick, der eine großartige Möglichkeit ist, motiviert zu bleiben, insbesondere wenn man seine Arbeit liebt.
- Dieses Projekt sollte die Grundlage für unsere Frontend-Codebasis für hoffentlich lange Zeit bilden, daher lohnte es sich, es richtig zu machen.
- Slash wächst und gewinnt, was das war GESAMT Motivation.
Teil 4: Wie geht es weiter?
Da die Einführung unseres Facelifts bereits in vollem Gange ist, liegt unsere Hauptpriorität nun darauf, Randfälle und Regressionen zu erkennen. Glücklicherweise verfügen wir über ein erstklassiges Support-Team, das unsere Kunden bei dieser Umstellung unterstützt und Probleme blitzschnell aufdeckt.
Wenn Sie sich unsere Arbeit ansehen möchten, finden Sie sie auf unserer Demo-Website, wo Sie auch die Funktionen von Slash kennenlernen können!
Wenn Sie es bis hierher geschafft haben und ein talentierter Ingenieur sind, der Probleme für echte Kunden in einem schnell wachsenden Start-up lösen möchte, bewerben Sie sich. hierWir würden uns freuen, von Ihnen zu hören.