Renovamos la imagen de Slash como parte de nuestra Recaudación de fondos de la serie B, y queríamos renovar visualmente nuestro producto junto con nuestra marca. Rápidamente nos dimos cuenta de que, además de actualizar nuestra interfaz de usuario, era una gran oportunidad para abordar gran parte de la deuda tecnológica que habíamos acumulado durante los últimos tres años de crecimiento extremadamente rápido.
Por lo tanto, proyecto Lifting facial nació: una oportunidad para sentar las bases del frontend de Slash para nuestro negocio y nuestro equipo en expansión. Queríamos tomar decisiones que resistieran el paso del tiempo aprendiendo de los errores del pasado.
Parte 1: Abordar la deuda técnica
En esta sección, hablaré sobre cada punto de la «deuda». Más adelante, en la parte 2, profundizaré en nuestras soluciones para cada punto.
Emotion: cuellos de botella en el tiempo de ejecución de CSS en JS
Nuestro sistema de diseño original creado sobre la base de emoción y nos ha funcionado muy bien. La principal ventaja de una biblioteca CSS en JS era la ubicación conjunta del marcado y el estilo de un componente, lo que agilizaba enormemente la iteración. Sin embargo, a medida que nuestros clientes crecían y tenían un volumen de datos cada vez mayor, empezamos a observar cuellos de botella en el rendimiento de Emotion. CSS en JS requiere una sobrecarga de tiempo de ejecución y puede ralentizar el bucle de eventos principal. Esto se nota al virtualizar listas grandes y desplazarse rápidamente. Cuando los componentes se montan y desmontan rápidamente, el cálculo dinámico de estilos y la generación de hojas de estilo afectan al navegador hasta el punto de que no puede renderizar a 60 fps. Otras empresas han encontró cuellos de botella similares en el rendimiento y se alejó de las soluciones dependientes de la sobrecarga de tiempo de ejecución.
«Componentes divinos» basados en accesorios
El concepto original de «Dios se oponeProviene de la programación orientada a objetos (OOP), donde un objeto asumiría demasiadas responsabilidades por sí solo. Observamos que ocurría algo similar en nuestros componentes React, a los que empezamos a llamar «componentes Dios».
Muchos de nuestros componentes originales de React se escribieron con una interfaz basada en props, en la que se proporcionan datos y callbacks y se renderiza un único componente. A primera vista, esto proporciona una interfaz elegante: basta con proporcionar datos y props y confiar en que el componente se encargará de todo el marcado y la lógica de negocio.
En la práctica, a medida que crece el código base, estos componentes se amplían para cubrir usos que no se habían considerado o previsto inicialmente. Cada vez que un ingeniero necesitaba una nueva configuración, añadía props, lo que dificultaba el mantenimiento y el uso con el paso del tiempo. Estos componentes acabaron convirtiéndose en «componentes divinos» que requerían cuidados para su mantenimiento, con props únicos destinados a resolver determinados problemas, o «escape hatches» destinados a inyectar/reemplazar marcas adicionales en el propio componente (similar a un patrón de render prop). Más adelante en el blog mostraré ejemplos de código.
Gestión inconsistente de formularios
Nuestra solución original para la gestión de formularios era simplemente la gestión de estados de React. El estado local es fácil de usar, pero carece de estandarización y de una seguridad de tipos profunda, lo que daba lugar a una calidad del código inconsistente. La forma en que cada ingeniero implementaba el estado local era ligeramente diferente: algunos aprovechaban el API de contexto con un único estado de formulario compartido, mientras que otros dividirían los campos en campos individuales. useState llamadas.
Cuando llegaba el momento de ampliar estos formularios, a menudo los metadatos indicaban que esCampoTocado se añadiría, creando un componente desordenado con muchos estados locales definidos explícitamente para gestionar interacciones de formularios que deberían estar estandarizadas.
Parte 2: Ejecución
Ahora que hemos esbozado los problemas actuales de nuestro código base, voy a profundizar en las soluciones y en la toma de decisiones que hay detrás de cada una de ellas.
Migración a Tailwind v4.0
Al evaluar las soluciones de estilo, teníamos dos finalistas: Tailwind v4.0 y PandaCSS, ya que ambos tienen una sobrecarga de tiempo de ejecución nula y permiten la coubicación del marcado y el estilo de un componente. Las ventajas de PandaCSS eran la seguridad de tipos y una curva de aprendizaje baja, ya que nuestro sistema de diseño existente era muy similar.
Las ventajas de Tailwind eran un ecosistema maduro y la familiaridad de los desarrolladores. Muchos de nuestros ingenieros habían utilizado Tailwind para proyectos personales y les gustaba bastante, incluido yo mismo. Hay herramientas fantásticas con soporte integrado para Tailwind, como Supernova y Variantes TailwindLas herramientas de codificación con IA también son excelentes para escribir nombres de clases Tailwind de forma inmediata, lo que hace que la migración sea mucho menos complicada.
Para visualizar las ventajas de rendimiento que supone no tener sobrecarga de tiempo de ejecución, aquí se muestran los gráficos de llama antes y después de la misma operación (desplazamiento rápido en una vista de tabla virtualizada grande) en Emotion y Tailwind:


Proceso de conversión de Figma a código
Al migrar a Tailwind, queríamos encontrar una forma de mantener sincronizados nuestros tokens y código base de Figma, siendo Figma nuestra fuente de verdad. Nuestro sistema de diseño en Figma lo mantiene nuestra agencia de diseño, Metacarbon, quien toma las decisiones de diseño y crea la hoja de estilos con tokens semánticos.
Nuestro objetivo era disponer de un canal en el que cualquier cambio en el sistema de diseño subyacente pudiera revisarse manualmente e implementarse con el mínimo esfuerzo. Elegimos Supernova para extraer los tokens de diseño de nuestro Figma e incorporarlos al código, ya que cuentan con un excelente extractor de código Tailwind v4.0.
Mientras trabajábamos con nuestra salida css, nos encontramos con un problema interesante: Queríamos que los tokens semánticos apuntaran a diferentes valores, dependiendo de la propiedad a la que se aplicaran. He aquí un ejemplo ilustrativo:
Queremos neutro-sutil-predeterminado resolver en 3 tonos diferentes de producto ligero neutro: 100, 500 y 1000 - sin embargo, si solo utilizáramos esto, Tailwind v4.0 --color variable de tema generaría bg-bg-neutral-subtle-default, fondo-frontera-neutro-sutil-predeterminado, bg-texto-neutro-sutil-predeterminado y muchos otros nombres de clase que no tienen sentido y son redundantes en su denominación.
Por ello, escribimos un script de compilación personalizado para transformar nuestra salida y utilizar las opciones más específicas de Tailwind v4.0. espacio de nombres de variables temáticas:
Lo que se resuelve en nombres de clase como bg-neutral-subtle-default, borde-neutro-sutil-predeterminado y texto-neutro-sutil-predeterminado - que se resuelven en sus distintos matices.
Por lo tanto, a medida que cambia nuestro sistema de diseño subyacente, solo tenemos que ejecutar nuestro exportador Supernova e introducir el resultado en nuestro código base para revisarlo. A continuación, nuestro script de compilación convierte automáticamente el resultado sin procesar en un archivo CSS compilado que consumen todos nuestros paquetes.
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.
Bibliotecas de interfaz de usuario sin cabeza: Base UI y React Aria
En primer lugar, quiero explicar por qué queríamos una biblioteca de interfaz de usuario sin encabezado y no una solución con estilo. El objetivo principal del cambio de imagen es que nuestra identidad de marca se exprese claramente a través de nuestro producto, y el uso de una solución con estilo predefinido como Shadcn o MaterialUI iría en contra de este propósito al aplicar un estilo predefinido a nuestros componentes.
Antes del rediseño, utilizábamos Radix UI, pero no estábamos seguro sobre su futuro, ya que vimos cómo los principales mantenedores pasaban a Base UI. Base UI se ha convertido en una biblioteca sencilla y extensible que se adapta muy bien a nuestras necesidades, con una gran accesibilidad. Dada la excelencia de sus mantenedores, Base UI nos da confianza en que en el futuro crecerá al ritmo de las necesidades de Slash.
Para ciertos componentes que aún no son compatibles con Base UI (por ejemplo, nuestro selector de fechas), optamos por React Aria, que fue nuestro segundo candidato en nuestra búsqueda de una interfaz de usuario sin cabeza. React Aria ha sido probado en combate y cumple estrictamente con todos los estándares de accesibilidad. Al final, creamos nuestros componentes sobre Base UI, ya que el enfoque de React Aria es bastante dogmático y nos obliga a aceptar por completo el ecosistema de React Aria, y preferimos algo que nos supusiera menos esfuerzo mental.
Componentes compuestos
Como se mencionó anteriormente, los componentes basados en accesorios son elegantes inicialmente, pero adolecen de una extensibilidad deficiente. Nuestra solución para crear componentes más extensibles fue apoyarnos en gran medida en el arquitectura de componentes compuestos, que transfiere el control del marcado de un componente a su padre. La mejor manera de ilustrar la diferencia entre ambos es con un ejemplo. A continuación se muestra una versión simplificada de nuestro antiguo y nuevo componente de selección con función de búsqueda:
Y aquí está nuestro nuevo componente select con función de búsqueda, que utiliza una arquitectura de componentes compuestos:
A primera vista, los componentes compuestos son más engorrosos: el desarrollador necesita conocer el patrón de marcado y seguirlo. La ventaja es la flexibilidad con la que se puede ampliar el marcado, ya que el control total recae en el padre. Por lo tanto, los trucos de marcado nunca llegan al componente principal. ¿Si un ingeniero necesita añadir un pie de página al select? Basta con añadirlo al marcado, sin necesidad de un seleccionar pie de página ¡Apoya al componente central! Si un pie de página se convierte en un requisito común, crea un <SearchableSelect.Footer /> componente!
Otro error habitual con los componentes compuestos es que los ingenieros pueden duplicar implementaciones de patrones comunes que no están en el componente principal. Por eso, nuestro equipo tiene que estar más atento a los patrones comunes y añadirlos al componente principal cuando sea necesario. También puedes usar componentes compuestos como API subyacente para un componente de una línea, pero lo hacemos de forma muy intencionada para no convertir ESTOS componentes en componentes divinos. En una sección posterior, explico con más detalle cómo abordamos el intercambio de conocimientos a través de Storybook.
Variantes Tailwind
A la hora de diseñar nuestros componentes, nos basamos en gran medida en Variantes TailwindAlgunos de nosotros hemos utilizado Autoridad de Variación de Clase (CVA) y queríamos ese tipo de lógica variante en nuestra solución de estilo. Tailwind Variants tenía un Un par de características que nos animaron a utilizarlo., siendo las principales la API de ranuras y la resolución de conflictos integrada.
En última instancia, solo necesitábamos una solución que mantuviera ordenado nuestro código de variantes de estilo y fuera lo suficientemente extensible como para cubrir nuestro enfoque de componentes compuestos. El uso de Tailwind Variants estandarizó significativamente nuestro antiguo código de estilo condicional, que aplicaba explícitamente diferentes tokens al div con estilo emocional en función de sus propiedades.
A continuación se muestra un ejemplo ilustrativo de la implementación y el uso de nuestro botón:
Como puede ver, quien llama a Botón en realidad no necesita usar botónVariantes - pasa los accesorios a Botón como opciones de configuración, que luego se utilizan para llamar a botónVariantes internamente. También utilizamos el extender Función para compartir estilos entre componentes similares, como campos de entrada y campos de selección.
También notarás lo fácil que es escanear los estilos gracias a nuestros nombres de clase semánticos.
Pruebas y documentación: Storybook + Chromatic
Durante la primera mitad del proyecto, trabajé solo y no le di a las pruebas y la documentación la importancia que merecían. Sin embargo, cuando terminé la biblioteca de componentes básica y se incorporaron más ingenieros para renovar sus respectivos productos, se hizo más evidente que necesitábamos un lugar centralizado para responder a preguntas muy sencillas como «¿tenemos este componente?» y «¿cómo se utiliza correctamente este componente?».
Uno de nuestros nuevos ingenieros, Sam, configuró por su cuenta nuestro conjunto de pruebas Storybook y Chromatic. Sin ellas, podríamos haber caído rápidamente en las mismas trampas de deuda tecnológica que supone el uso indebido de componentes, especialmente teniendo en cuenta la curva de aprendizaje de los componentes compuestos.
Storybook nos permite centralizar ejemplos de uso para todos los componentes, lo que permite responder a esas preguntas de forma completa con ejemplos de código. También queda claro de inmediato qué componente debe utilizar un ingeniero y si es compatible con las opciones de configuración que necesita.
Chromatic detecta regresiones visuales, de modo que cualquier cambio en los componentes subyacentes se señalará inmediatamente si modifica algún elemento visual, bloqueando la fusión hasta que se revise manualmente.
Gestión de formularios: Zod vs ArkType
El código base de Slash está escrito íntegramente en Typescript, y aprovechamos al máximo esa circunstancia generando tipos compartidos en todo nuestro código base en el momento de la compilación. Queríamos una solución de gestión de formularios que dificultara a los desarrolladores cometer errores y que aprovechara nuestra amplia seguridad de tipos.
Al final elegimos ArkType, ya que estaba tan estrechamente vinculado a Typescript que podíamos evitar cualquier desviación. Podíamos tomar nuestros tipos generados y vincularlos uno a uno con los tipos de ArkType utilizando satisface, de modo que si nuestro tipo subyacente cambia, nuestro código base generará errores de tipo.
He aquí un ejemplo ilustrativo:
Aunque Zod y React Hook Form podrían habernos servido bien, queríamos algo que aprovechara directamente TypeScript, y ArkType era claramente el líder en este aspecto. Elegimos Tanstack Form en lugar de React Hook Form debido a su mayor adherencia a TypeScript.
Una cosa que notarás con el formulario Tanstack es cómo te obliga a seguir una estructura estricta y definida. Aunque esto supone una curva de aprendizaje, creemos que vale la pena para garantizar la estandarización en la forma en que escribimos los formularios en nuestro código base.
Implementación: Indicador de función y ramificación de código
A la hora de escribir el código para el rediseño, queríamos evitar una PR enorme, ya que seguíamos lanzando nuevas funciones en el antiguo sistema de diseño junto con el rediseño.
Utilizamos un indicador de función, y simplemente habilité la bandera de función para solo nuestra cuenta, lo que nos permite comida para perros nuestros propios cambios. También implementamos una sencilla barra de herramientas para activar y desactivar el cambio de aspecto, lo que facilita enormemente la detección de regresiones al poder activar y desactivar rápidamente el cambio de aspecto.

En cuanto a la ramificación del código real, nuestro objetivo era no duplicar el código de la lógica de negocio, y lo hicimos de la siguiente manera:
El ejemplo anterior ilustra un caso ideal. - La realidad de introducir grandes cantidades de código nuevo en una base de código existente es mucho más complicada. Por ejemplo: si realizas la isFacelift Si se cambia un nivel superior (por ejemplo, en la página principal), es necesario duplicar toda la lógica de negocio para todas las ramas que se encuentran debajo de ese componente. Sin embargo, si se realiza la comprobación a nivel de cada componente individual, se está sujeto a la antigua lógica de marcado del elemento principal, que a menudo necesita una renovación.
Lo que nos protegió de los problemas de producción fue ocultar toda esta funcionalidad detrás de un indicador de características, de modo que para todos los clientes reales, isFacelift siempre fue falso hasta que estuviéramos listos. Internamente, lo mantendríamos activado para poder detectar regresiones a medida que se introdujeran en la plataforma.
A medida que la renovación se completaba, nos asociamos con clientes con los que teníamos una relación estrecha y les ofrecimos la opción de desactivarla, solicitándoles comentarios y detectando regresiones sin bloquear ningún flujo de trabajo existente.
Notas sobre la ejecución
Aunque la sección anterior está organizada de forma muy ordenada, este no fue en absoluto el orden de implementación. En realidad, estas decisiones se tomaron probando cosas y repitiéndolas rápidamente. Por ejemplo,
- Mi primer borrador del script de compilación CSS enviaba spam al
@utilidaddirectiva para crear cada nombre de clase personalizado que queríamos; luego, Sam lo desarrolló aún más al encontrar esto. debate en GitHub que describía los espacios de nombres variables del tema «tailwind». - Hubo dos iteraciones de nuestro componente de selección única con función de búsqueda que utilizaban diferentes componentes BaseUI antes de que nos decantáramos por nuestro enfoque actual, utilizando el API del menú.
Todo esto para decir que no tomamos estas decisiones con tanta elegancia como las compensaciones descritas anteriormente. El hilo conductor es la iteración constante sin la parálisis de sentir que hay que hacerlo perfecto a la primera.
Parte 3: Aprendizajes
Personalmente, aprendí mucho de un proyecto cuyo alcance abarcaba todo el frontend. Nuestro equipo también aprendió mucho, especialmente a medida que más ingenieros contribuían a renovar sus respectivos productos.
«Elevar el nivel mínimo»
Una de las principales razones por las que se acumuló la deuda tecnológica fue la falta de intercambio de conocimientos. En una startup joven esto no supone un problema, ya que todos trabajan en estrecha colaboración. Pero a medida que el equipo crece y el producto se expande, resulta imposible compartir hipótesis y patrones de uso con la misma rapidez que antes.
Nuestra filosofía para el rediseño fue «elevar el nivel». Queremos que sea difícil escribir código frontend deficiente, lo que significa documentar cómo es un buen código frontend y detectar los errores antes de que lleguen a producción. Storybook y Chromatic son fundamentales para ello, y vale la pena invertir en su configuración. Cada esfuerzo que dediques a elevar el nivel tendrá un rendimiento compuesto en la calidad del código en toda la base de código, y lo contrario también es cierto.
La realidad de los proyectos largos
El rediseño, de principio a fin, llevó alrededor de cuatro meses de trabajo continuo, con una «semana de hackeo» dedicada al final que duró tres semanas y en la que participaron otros seis ingenieros que contribuyeron al rediseño (mucho amor para ellos <3). También conté con un excelente gestor de proyectos (Andy) que me ayudó a definir el alcance de todo el proyecto, a comunicarme con Metacarbon y a proporcionar comentarios sobre los nuevos patrones de experiencia de usuario.
Dado que el rediseño no era un proyecto que pudiera lanzar por partes, no pude disfrutar de la satisfacción de lanzar el producto a los clientes durante mucho tiempo. Aprendí que un buen enfoque es cambiar de contexto internamente: a veces trabajaba en la biblioteca de componentes básicos y luego aplicaba esos componentes básicos a algunas páginas completas, y alternaba entre ambas tareas. Este enfoque también te ayuda a detectar rápidamente los casos extremos en el uso de los componentes básicos, lo que te obliga a volver atrás y trazar mejores líneas de abstracción.
Puede resultar desmoralizador refactorizar toneladas de código sin que los clientes lo aprecien. Lo que me mantuvo motivado fueron un par de cosas:
- Ir al trabajo seguía siendo divertido cada día gracias a la gente de Slash.
- Aún así, podía presumir de mis progresos internamente. Obtener una especie de dopamina al mostrar a los demás los progresos que estás haciendo es una forma estupenda de mantener la motivación, especialmente si te encanta tu trabajo.
- Este proyecto iba a sentar las bases de nuestro código frontend durante, esperemos, mucho tiempo, por lo que merecía la pena hacerlo bien.
- Slash está creciendo y ganando, lo cual era el GENERAL motivación.
Parte 4: ¿Qué sigue?
Con el lanzamiento de nuestro rediseño ya en marcha, nuestra principal prioridad es detectar los casos extremos y las regresiones. Por suerte, contamos con un equipo de asistencia de primer nivel que ayuda a nuestros clientes durante esta transición y detecta los problemas a la velocidad del rayo.
Si desea ver nuestro trabajo, puede hacerlo en nuestra página web. sitio de demostración, donde también puedes ver las funciones que ofrece Slash.
Si has llegado hasta aquí y eres un ingeniero con talento que busca resolver problemas para clientes reales en una startup en rápido crecimiento, envía tu solicitud. aquíNos encantaría saber de ti.