Renomeamos a Slash como parte da nossa Captação de recursos da Série B, e queríamos renovar visualmente nosso produto junto com nossa marca. Rapidamente percebemos que, além de atualizar nossa interface do usuário, essa era uma ótima oportunidade para resolver grande parte da dívida técnica que acumulamos nos últimos três anos de crescimento extremamente rápido.
Assim, projeto Lifting facial nasceu - uma oportunidade para estabelecermos as bases para o front-end da Slash para o nosso negócio e equipe em expansão. Queríamos tomar decisões que resistissem ao teste do tempo, aprendendo com os erros do passado.
Parte 1: Lidando com a dívida técnica
Nesta seção, abordarei cada ponto relacionado à “dívida”. Mais adiante, na parte 2, aprofundarei nossas soluções para cada ponto.
Emoção: gargalos de tempo de execução do CSS no JS
Nosso sistema de design original desenvolvido com base em emoção e nos serviu bem. A principal vantagem de uma biblioteca CSS em JS era a co-localização da marcação e do estilo de um componente, o que tornava a iteração extremamente rápida. No entanto, à medida que nossos clientes cresciam e tinham um volume de dados cada vez maior, começamos a observar gargalos de desempenho no Emotion. O CSS em JS requer sobrecarga de tempo de execução e pode tornar o loop de eventos principal mais lento. Isso é perceptível ao virtualizar listas grandes e rolar rapidamente. Quando os componentes são montados e desmontados rapidamente, o cálculo dinâmico de estilos e a geração de folhas de estilo afetam o navegador a ponto de ele não conseguir renderizar a 60 fps. Outras empresas têm encontrou gargalos de desempenho semelhantes e afastou-se das soluções dependentes da sobrecarga de tempo de execução.
“Componentes divinos” baseados em adereços
O conceito original de “Objeções de Deus” tem origem na OOP, onde um objeto assumiria demasiadas responsabilidades sozinho. Observamos situações semelhantes nos nossos componentes React, aos quais começamos a chamar de “componentes divinos”.
Muitos dos nossos componentes originais do React foram escritos com uma interface baseada em props, na qual você fornece dados e callbacks e renderiza um único componente. À primeira vista, isso proporciona uma interface elegante: basta fornecer dados e props e confiar que o componente cuidará de toda a marcação e lógica de negócios.
Na prática, à medida que a base de código cresce, esses componentes são ampliados para cobrir usos que não foram originalmente considerados ou pretendidos. Cada vez que um engenheiro precisava de uma nova configuração, ele adicionava props, tornando a manutenção e o uso mais difíceis com o tempo. Esses componentes acabaram se tornando “componentes divinos” que precisavam de cuidados para serem mantidos, com props únicos destinados a resolver certos problemas ou “saídas de emergência” destinadas a injetar/substituir marcações adicionais no próprio componente (semelhante a um padrão de render prop). Mostrarei exemplos de código mais adiante no blog.
Gerenciamento inconsistente de formulários
Nossa solução original de gerenciamento de formulários era simplesmente o gerenciamento de estado do React. O estado local é fácil de usar, mas carece de padronização e segurança de tipo profunda, o que levava a uma qualidade inconsistente do código. A maneira como cada engenheiro implementava o estado local era um pouco diferente — alguns aproveitavam o API de contexto com um grande estado de formulário compartilhado, enquanto outros dividiriam os campos em individuais useState chamadas.
Quando chegou a hora de estender esses formulários, muitas vezes metadados como campoTocado seria adicionado, criando um componente confuso com muitos estados locais explicitamente definidos para lidar com interações de formulário que deveriam ser padronizadas.
Parte 2: Execução
Agora que descrevemos as dificuldades atuais da nossa base de código, vou me aprofundar nas soluções e na tomada de decisões por trás de cada uma delas.
Migração para o Tailwind v4.0
Ao avaliar as soluções de estilo, tivemos dois finalistas: Tailwind v4.0 e PandaCSS, pois ambos têm zero sobrecarga de tempo de execução e permitem a co-localização da marcação e do estilo de um componente. As vantagens do PandaCSS eram a segurança de tipos e uma curva de aprendizado baixa, já que nosso sistema de design existente era muito semelhante.
As vantagens do Tailwind eram um ecossistema maduro e a familiaridade dos desenvolvedores. Muitos de nossos engenheiros já haviam usado o Tailwind em projetos pessoais e gostavam bastante dele, inclusive eu. Existem ferramentas fantásticas com suporte integrado ao Tailwind, como Supernova e Variantes TailwindAs ferramentas de codificação de IA também são excelentes para escrever nomes de classes Tailwind prontos para uso, tornando a migração muito menos trabalhosa.
Para visualizar as vantagens de desempenho de não ter sobrecarga de tempo de execução, aqui estão os gráficos de chama antes e depois da mesma operação (rolagem rápida em uma visualização de tabela virtualizada grande), no Emotion e no Tailwind:


Pipeline do Figma para o código
Ao migrar para o Tailwind, queríamos uma maneira de manter nossos tokens e código-fonte do Figma sincronizados, com o Figma sendo nossa fonte de verdade. Nosso sistema de design no Figma é mantido por nossa agência de design, Metacarbonato, que toma as decisões de design e cria a folha de estilo com tokens semânticos.
Nosso objetivo era ter um pipeline em que quaisquer alterações no sistema de design subjacente pudessem ser revisadas manualmente e implementadas com o mínimo de esforço. Escolhemos a Supernova para extrair tokens de design do nosso Figma e transformá-los em código, pois eles têm um excelente extrator de código Tailwind v4.0.
Ao trabalhar com nossa saída CSS, encontramos um problema interessante: Queríamos que os tokens semânticos apontassem para valores diferentes, dependendo da propriedade à qual se aplicavam. Aqui está um exemplo ilustrativo:
Queremos neutro-sutil-padrão para resolver em 3 tons diferentes de neutro em relação aos produtos leves: 100, 500 e 1000 - no entanto, se usássemos apenas isso, o Tailwind v4.0 --cor variável de tema geraria bg-bg-neutro-sutil-padrão, fronteira-bg-neutro-sutil-padrão, bg-texto-neutro-sutil-padrão e muitos outros nomes de classe que não fazem sentido e são redundantes na nomenclatura.
Assim, escrevemos um script de compilação personalizado para transformar nossa saída e utilizar o Tailwind v4.0 mais específico. espaço de nomes das variáveis do tema:
O que resulta em nomes de classe como bg-neutro-sutil-padrão, fronteira-neutra-sutil-padrão e texto neutro sutil padrão - que se resolvem em seus distintos tons.
Assim, à medida que nosso sistema de design subjacente muda, basta executar nosso exportador Supernova e colocar a saída em nossa base de código para revisão. Em seguida, nosso script de compilação converte automaticamente a saída bruta em um arquivo CSS compilado que todos os nossos pacotes consomem.
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 interface do usuário sem cabeçalho: Base UI e React Aria
Em primeiro lugar, gostaria de explicar por que queríamos uma biblioteca de interface do usuário sem cabeçalho, e não uma solução estilizada. O objetivo da reformulação é que nossa identidade de marca seja expressa claramente por meio de nosso produto — e usar uma solução pré-estilizada como Shadcn ou MaterialUI iria contra esse objetivo, pois pré-estilizaria nossos componentes.
Antes da reformulação, usávamos a Radix UI, mas não estávamos certo sobre o seu futuro, à medida que vimos os principais mantenedores migrarem para o Base UI. O Base UI surgiu como uma biblioteca simples e extensível que se adapta muito bem às nossas necessidades, com grande aderência à acessibilidade. Dados seus excelentes mantenedores, o Base UI nos dá confiança no futuro de que ele crescerá junto com as necessidades do Slash.
Para determinados componentes que ainda não têm suporte para a interface de usuário Base (por exemplo, nosso seletor de datas), optamos pelo React Aria, que foi o segundo colocado em nossa busca por uma interface de usuário headless. O React Aria foi testado em batalha e segue rigorosamente todos os padrões de acessibilidade. No final das contas, criamos nossos componentes com base na Base UI, já que a abordagem do React Aria é bastante opinativa e exige que a gente se comprometa totalmente com o ecossistema React Aria, e a gente preferiu algo com menos sobrecarga mental.
Componentes compostos
Como mencionado anteriormente, os componentes baseados em props são inicialmente elegantes, mas sofrem com a baixa extensibilidade. Nossa solução para criar componentes mais extensíveis foi apoiar-nos fortemente no arquitetura de componentes compostos, que transfere o controle de marcação de um componente para seu pai. A melhor maneira de ilustrar a diferença entre os dois é com um exemplo — aqui está uma versão simplificada do nosso componente de seleção pesquisável antigo e novo:
E aqui está nosso novo componente selecionável pesquisável, que usa uma arquitetura de componentes compostos:
À primeira vista, os componentes compostos são mais complicados — o desenvolvedor precisa conhecer o padrão de marcação e segui-lo. A vantagem é a flexibilidade para estender a marcação, já que o controle total fica com o pai. Assim, os hacks de marcação nunca chegam ao componente principal. Se um engenheiro precisa adicionar um rodapé ao select? Basta adicioná-lo à marcação — sem necessidade de um renderizarSeleçãoRodapé prop para o componente principal! Se um rodapé se tornar um requisito comum, crie um <SearchableSelect.Footer /> componente!
Outra armadilha comum dos componentes compostos é que os engenheiros podem duplicar implementações de padrões comuns que não estão no componente principal. Assim, nossa equipe precisa estar mais atenta aos padrões comuns, adicionando-os ao componente principal quando necessário. Você também pode usar componentes compostos como API subjacente para um componente de uma linha, mas fazemos isso de forma muito intencional para não transformar ESSES componentes em componentes divinos. Abordo mais detalhadamente como lidamos com o compartilhamento de conhecimento por meio do Storybook em uma seção posterior.
Variantes Tailwind
Ao estilizar nossos componentes, contamos muito com Variantes TailwindAlguns de nós já usamos Autoridade de Variação de Classe (CVA) e queríamos esse tipo de lógica variante em nossa solução de estilo. O Tailwind Variants tinha um algumas funcionalidades que nos levaram a utilizá-lo, sendo as principais a API de slots e a resolução de conflitos integrada.
No final das contas, precisávamos apenas de uma solução que mantivesse nosso código de variantes de estilo organizado e fosse extensível o suficiente para cobrir nossa abordagem de componentes compostos. O uso das variantes do Tailwind padronizou significativamente nosso antigo código de estilo condicional, que aplicava explicitamente diferentes tokens ao div com estilo emocional com base em suas propriedades.
Abaixo está um exemplo ilustrativo da implementação e utilização do nosso botão:
Como você pode ver, quem faz a chamada de Botão não precisa realmente usar botãoVariantes - passa props para Botão como opções de configuração, que são então utilizadas para chamar botãoVariantes internamente. Também utilizamos o estender Recurso para compartilhar estilos entre componentes semelhantes, como campos de entrada e campos de seleção.
Você também perceberá como é fácil analisar os estilos devido aos nossos nomes de classe semânticos.
Testes e documentação: Storybook + Chromatic
Na primeira metade do projeto, trabalhei sozinho e não dei a devida importância aos testes e à documentação. No entanto, à medida que fui concluindo a biblioteca de componentes básicos e mais engenheiros se juntaram para renovar seus respectivos produtos, ficou mais evidente que precisávamos de um local centralizado para responder a perguntas muito simples, como “temos esse componente?” e “como uso esse componente corretamente?”.
Um dos nossos novos engenheiros, Sam, configurou sozinho nosso conjunto de testes Storybook e Chromatic. Sem eles, poderíamos ter caído rapidamente nas mesmas armadilhas da dívida técnica do uso indevido de componentes, especialmente considerando a curva de aprendizado dos componentes compostos.
O Storybook nos permite centralizar exemplos de uso para todos os componentes, permitindo que essas perguntas sejam respondidas na íntegra com exemplos de código. Também fica imediatamente claro qual componente um engenheiro deve usar e se ele suporta ou não as opções de configuração de que precisa.
O Chromatic detecta regressões visuais, de modo que quaisquer alterações nos componentes subjacentes serão sinalizadas imediatamente se alterarem qualquer elemento visual, bloqueando a fusão até que sejam revisadas manualmente.
Gerenciamento de formulários: Zod vs ArkType
A base de código do Slash é escrita inteiramente em Typescript, e aproveitamos bastante esse fato gerando tipos compartilhados em toda a nossa base de código no momento da compilação. Queríamos uma solução de gerenciamento de formulários que dificultasse aos desenvolvedores cometerem erros e aproveitássemos nossa ampla segurança de tipos.
Acabamos escolhendo o ArkType, pois ele era tão fortemente acoplado ao Typescript que podíamos evitar qualquer desvio. Podíamos pegar nossos tipos gerados e acoplá-los um a um com os tipos do ArkType usando satisfaz, de modo que, se nosso tipo subjacente mudar, nossa base de código lançará erros de tipo.
Aqui está um exemplo ilustrativo:
Embora o Zod e o React Hook Form pudessem ter nos servido bem, queríamos algo que aproveitasse diretamente o TypeScript, e o ArkType era claramente o líder nesse aspecto. Escolhemos o Tanstack Form em vez do React Hook Form devido à sua adesão mais rigorosa ao TypeScript.
Uma coisa que você notará com o formulário Tanstack é como ele o obriga a seguir uma estrutura rígida e padronizada. Embora isso exija um certo aprendizado, achamos que vale a pena para garantir a padronização na forma como escrevemos formulários em nossa base de código.
Implementação: sinalizador de recurso e ramificação de código
Quando chegou a hora de escrever o código para o Facelift, queríamos evitar um PR enorme, já que ainda estávamos lançando novos recursos no sistema de design antigo junto com o facelift.
Utilizamos um sinalizador de recursoe simplesmente habilitou o sinalizador de recurso para apenas nossa conta, permitindo-nos ração para cães nossas próprias alterações. Também implementamos uma barra de ferramentas simples para ativar e desativar o facelift, tornando extremamente fácil identificar regressões ao ativar e desativar rapidamente o facelift.

Quanto à ramificação do código propriamente dita, nosso objetivo era não duplicar o código da lógica de negócios, fazendo o seguinte:
O exemplo acima ilustra um caso ideal. - a realidade de introduzir grandes quantidades de código novo em uma base de código existente é muito mais complicada. Por exemplo: se você executar o éFacelift alterar um nível superior (na página inicial, por exemplo), você precisa duplicar toda a lógica de negócios para todos os ramos abaixo desse componente. Mas se você realizar a verificação no nível do componente individual, ficará preso à lógica de marcação antiga do pai, que muitas vezes precisa de uma reformulação.
O que nos protegeu de problemas de produção foi ocultar toda essa funcionalidade por trás de um sinalizador de recurso, de modo que, para todos os clientes reais, éFacelift sempre foi falso até estarmos prontos. Internamente, manteríamos isso ativado para que pudéssemos detectar regressões à medida que elas chegassem à plataforma.
À medida que a reformulação ficou completa, fizemos parcerias com clientes com os quais tínhamos um relacionamento próximo e disponibilizamos a opção de desativá-la, solicitando feedback e identificando regressões sem bloquear nenhum fluxo de trabalho existente.
Notas sobre a execução
Embora a seção acima esteja organizada de forma muito ordenada, essa não foi, de forma alguma, a ordem de implementação. Na realidade, essas decisões foram tomadas por meio de tentativas e iterações rápidas. Por exemplo,
- Meu primeiro rascunho do nosso script de compilação CSS encheu de spam o
@utilidadediretiva para criar cada nome de classe personalizado que queríamos - então Sam desenvolveu a partir disso, encontrando isto discussão no GitHub que descreveu os espaços de nomes variáveis do tema tailwind. - Houve duas iterações do nosso componente de seleção única pesquisável usando diferentes componentes BaseUI antes de decidirmos pela nossa abordagem atual, usando o API do menu.
Tudo isso para dizer que não tomamos essas decisões de forma tão elegante quanto as compensações descritas acima. O traço comum é a iteração constante, sem a paralisia de sentir que você precisa acertar na primeira tentativa.
Parte 3: Aprendizados
Eu, pessoalmente, aprendi muito com um projeto cujo escopo abrangia todo o front-end. Nossa equipe também aprendeu muito, especialmente à medida que mais engenheiros contribuíram para renovar seus respectivos produtos.
“Elevando o piso”
Uma das principais razões pelas quais a dívida técnica se acumulou, em primeiro lugar, foi a falta de compartilhamento de conhecimento. Em uma startup jovem, isso não é um problema, pois todos trabalham em estreita colaboração. Mas, à medida que a equipe cresce e a área de atuação do produto se expande, torna-se impossível compartilhar suposições e padrões de uso tão rapidamente quanto antes.
Nossa filosofia para a reformulação foi “elevar o nível”. Queremos dificultar a escrita de código front-end ruim, o que significa documentar como é um bom código front-end e detectar erros antes que eles cheguem à produção. O Storybook e o Chromatic são essenciais para isso e valem bem o investimento para serem configurados. Cada esforço que você dedicar para elevar o nível terá retornos compostos na qualidade do código em toda a base de código — e o inverso também é verdadeiro.
A realidade dos projetos longos
A reformulação do início ao fim levou cerca de quatro meses de trabalho contínuo, com uma “semana de hack” dedicada no final, que durou três semanas e envolveu outros seis engenheiros que contribuíram para a reformulação (muito amor para eles <3). Também tive um excelente gerente de projetos (Andy), que ajudou a definir o escopo de todo o projeto, comunicar-se com a Metacarbon e fornecer feedback sobre novos padrões de experiência do usuário.
Como a reformulação não era um projeto que eu pudesse lançar em partes, não tive a alegria de lançar o produto para os clientes por um longo tempo. Aprendi que uma boa abordagem é alternar o contexto internamente — às vezes, eu trabalhava na biblioteca de componentes principais e, em seguida, aplicava esses componentes principais a algumas páginas completas, alternando entre as duas tarefas. Essa abordagem também ajuda a detectar rapidamente casos extremos no uso dos componentes principais, forçando você a voltar e traçar melhores linhas de abstração.
Pode ser desmoralizante refatorar toneladas de código sem receber nenhum reconhecimento dos clientes. O que me manteve motivado foram algumas coisas:
- Ir trabalhar continuava sendo divertido todos os dias por causa das pessoas da Slash!
- Eu ainda poderia mostrar o progresso internamente. Receber uma espécie de dopamina ao mostrar a alguém o progresso que você está fazendo é uma ótima maneira de se manter motivado, especialmente se você ama o que faz.
- Este projeto iria estabelecer as bases para nossa base de código front-end por um longo tempo, esperamos, então valia a pena fazê-lo da maneira certa.
- Slash está crescendo e vencendo, o que era o GERAL motivação.
Parte 4: O que vem a seguir
Com o lançamento da nossa reformulação em andamento, nossa principal prioridade passa a ser detectar casos extremos e regressões. Felizmente, contamos com uma equipe de suporte de nível internacional que auxilia nossos clientes nessa transição e identifica problemas com rapidez.
Se você quiser conferir nosso trabalho, pode vê-lo em nosso site de demonstração, onde você também pode conferir os recursos que o Slash oferece!
Se você chegou até aqui e é um engenheiro talentoso que deseja resolver problemas para clientes reais em uma startup em rápido crescimento, inscreva-se. aquiAdoraríamos receber seu contato.