Lit e Web Components em projetos reais: experiência após 6 meses de uso

1. Por que escolhemos Lit e Web Components?

A fragmentação de frameworks frontend é um problema real. Em um mesmo ecossistema corporativo, é comum encontrar React em um módulo, Angular em outro e Vue em um terceiro. Cada time defende sua escolha, e o resultado é uma base de código heterogênea, difícil de manter e com componentes duplicados.

A proposta dos Web Components — Custom Elements, Shadow DOM e Slots — sempre foi atraente: criar elementos reutilizáveis independentes de framework. Porém, a API nativa é verbosa e propensa a erros. Foi aí que o Lit entrou.

Lit é uma biblioteca leve (cerca de 5 KB minificada) que abstrai a complexidade dos Web Components, oferecendo reatividade declarativa, templates baseados em tagged literals e um sistema de propriedades reativas. Após 6 meses de uso intenso, posso afirmar que a escolha foi acertada.

Comparando com alternativas:
- Stencil: mais opinativo, gera componentes com bundler próprio, mas adiciona complexidade de build.
- Vanilla Web Components: viável para componentes simples, mas a verbosidade para gerenciar estado e ciclo de vida torna inviável em escala.
- Lit: equilíbrio entre simplicidade e poder, com curva de aprendizado suave para quem já conhece JavaScript moderno.

2. Setup inicial e primeiras impressões

Iniciamos o projeto com Vite + Lit. O boilerplate é mínimo:

npm create vite@latest my-components -- --template lit-ts

O primeiro componente criado foi um botão encapsulado:

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-button')
export class MyButton extends LitElement {
  static styles = css`
    :host {
      display: inline-block;
    }
    button {
      padding: 8px 16px;
      border: none;
      border-radius: 4px;
      background: var(--primary-color, #6200ee);
      color: white;
      cursor: pointer;
    }
    button:hover {
      opacity: 0.9;
    }
  `;

  @property({ type: String }) label = 'Clique aqui';
  @property({ type: Boolean }) disabled = false;

  render() {
    return html`
      <button ?disabled="${this.disabled}">
        <slot></slot>
        ${this.label}
      </button>
    `;
  }
}

A primeira impressão foi surpreendente. O Shadow DOM isola estilos nativamente — sem CSS-in-JS, sem módulos CSS, sem preocupações com vazamento. O desenvolvimento fluiu de forma similar a React, mas sem a necessidade de hooks ou contextos complexos.

3. Arquitetura de componentes no mundo real

Adotamos o design atômico para organizar os componentes:

src/
  components/
    atoms/        # botão, input, ícone
    molecules/    # campo de formulário, card
    organisms/    # navbar, tabela, modal
    templates/    # layout de página

A comunicação entre componentes foi um ponto de atenção. Inicialmente usamos apenas propriedades reativas (@property), mas rapidamente percebemos a necessidade de eventos customizados para comunicação bidirecional:

// Componente pai escuta evento
<meu-formulario @submit="${this.handleSubmit}">
  <meu-input name="email"></meu-input>
</meu-formulario>

// Componente filho dispara evento
this.dispatchEvent(new CustomEvent('submit', {
  detail: { email: this.value },
  bubbles: true,
  composed: true
}));

Para estado global, integramos com RxJS:

import { Subject } from 'rxjs';

const store = new Subject();
export const state$ = store.asObservable();

export function updateState(newState) {
  store.next(newState);
}

// No componente
connectedCallback() {
  super.connectedCallback();
  this.subscription = state$.subscribe(state => {
    this.user = state.user;
  });
}

disconnectedCallback() {
  super.disconnectedCallback();
  this.subscription?.unsubscribe();
}

4. Performance e renderização na prática

A reatividade do Lit é eficiente. O decorator @property marca propriedades que disparam re-renderização apenas quando alteradas. O método shouldUpdate permite controle fino:

shouldUpdate(changedProperties) {
  return changedProperties.has('visible');
}

O Shadow DOM realmente isola estilos. Em 6 meses, não tivemos um único caso de vazamento CSS entre componentes. A desvantagem é que estilos globais (como variáveis CSS) precisam ser herdados explicitamente via :host ou part.

Benchmarks reais do nosso projeto (uma SPA de dashboard com 40+ componentes):
- Tempo de carregamento inicial: 1.2s (vs. 1.8s com React equivalente)
- First Paint: 0.4s
- Interatividade: 0.9s

A ausência de um virtual DOM pesado faz diferença em dispositivos móveis.

5. Integração com frameworks e sistemas legados

O maior teste foi integrar Web Components dentro de uma aplicação React existente. A solução foi simples:

// React wrapper
function MyButtonWrapper({ label, onClick }) {
  const ref = useRef(null);

  useEffect(() => {
    const el = ref.current;
    el.addEventListener('click', onClick);
    return () => el.removeEventListener('click', onClick);
  }, [onClick]);

  return <my-button ref={ref} label={label}></my-button>;
}

Com Angular e Vue, a integração foi ainda mais natural, pois ambos possuem suporte nativo a Custom Elements.

Um caso real de sucesso: migramos gradualmente um monólito jQuery de 50 mil linhas. Criamos Web Components para substituir partes da interface, e o sistema legado consumia os novos componentes via document.createElement. Sem rewrite, sem quebra de funcionalidades.

6. Manutenção e evolução após 6 meses

O versionamento de componentes foi resolvido com tags semânticas no npm:

@my-org/button@1.2.0
@my-org/table@2.0.0

Testes com Web Test Runner se mostraram robustos:

import { expect, fixture, html } from '@open-wc/testing';
import '../src/my-button.js';

describe('MyButton', () => {
  it('renderiza com label padrão', async () => {
    const el = await fixture(html`<my-button></my-button>`);
    expect(el.shadowRoot.textContent).to.include('Clique aqui');
  });
});

Lições aprendidas:
- Funcionou bem: encapsulamento de estilo, reatividade simples, integração com qualquer framework.
- Gerou retrabalho: a falta de um ecossistema de componentes prontos (como Material-UI) nos obrigou a construir tudo do zero. Também enfrentamos dificuldades com acessibilidade em alguns componentes customizados.

7. Comparação final: Lit vale a pena?

Pontos fortes:
- Leveza e performance superiores
- Padronização nativa (Web Components são parte do HTML)
- Independência total de framework — componentes funcionam em qualquer lugar

Pontos fracos:
- Ecossistema menor que React/Vue
- Curva de aprendizado para times acostumados com JSX e hooks
- Ferramentas de debug menos maduras (extensão Chrome para Lit ainda é limitada)

Recomendação baseada em cenários:
- SPAs complexas: Lit é viável, mas faltam bibliotecas de roteamento e estado maduras. Prefira React se o time já tem expertise.
- Micro-frontends: Lit é a escolha ideal. Componentes independentes que funcionam em qualquer shell.
- Design Systems: Lit é excelente. Componentes encapsulados, leves e portáteis.

Após 6 meses, a decisão se mostrou acertada para nosso contexto de design system corporativo com múltiplos times usando frameworks diferentes. A padronização trouxe consistência, a performance agradou e a manutenção se provou sustentável.


Referências