40 minutos, 85% mais rápido: o que a IA fez na minha lib de CPF e CNPJ
Eu tenho uma biblioteca que acompanha eu não sei quantos projetos: validação de CPF e CNPJ em Node.js. É daquelas que você escreve uma vez, publica no npm e passa a arrastar de projeto em projeto. Funcionava, tinha teste, ninguém mexia.
Só que a Receita Federal mudou as regras. Desde julho de 2026, o CNPJ de novas inscrições pode ter letras, além de números. Ou seja: aquela validação que assumia 14 dígitos numéricos parou de cobrir os documentos novos. Hora de mexer na lib.
E aqui vai o detalhe que me interessava contar: foi a primeira vez que eu atualizei essa biblioteca com ajuda de IA, de ponta a ponta. Feature nova, revisão completa do código e uma rodada de otimização. Tudo isso em cerca de 40 minutos. O que eu quero mostrar aqui é como foi feito — porque o número bonito sem o processo não prova nada.
A feature: CNPJ alfanumérico
Eu não mandei a IA “implementar CNPJ alfanumérico” e torcer. Escrevi uma especificação primeiro, descrevendo o que mudou no formato e no cálculo do dígito verificador. No repositório egermano/skills eu mantenho skills de TDD e XP, e foram elas que guiaram o trabalho: primeiro os testes com os exemplos do manual oficial da Receita (como o 12.ABC.345/01DE-35), depois a implementação mínima pra fazer eles passar.
O resultado foi o esperado: feature entregue, os 109 testes verdes, cobertura em 100%. Até aqui, qualquer sessão de TDD bem conduzida — com ou sem IA — teria chegado no mesmo lugar.
A revisão: onde apareceu o dinheiro
Aí eu pedi outra coisa: revisar o código inteiro e propor melhorias. E foi essa segunda etapa que rendeu mais que a primeira. A revisão apontou trabalho em três eixos, nesta ordem de prioridade: corrigir comportamento, reduzir complexidade e melhorar performance.
Um bug de verdade
Rodando o código, apareceu isso:
cpfValidator("77325121890123"); // retornava true (!)
A implementação antiga extraía os caracteres numéricos, ignorava qualquer excedente e validava só os 11 primeiros dígitos. Ou seja: uma string de 14 dígitos cujo prefixo é um CPF válido passava como CPF. A correção, em TDD estrito — teste novo vermelho, correção mínima, verde — foi rejeitar entradas com mais de 11 dígitos depois da limpeza.
Três decisões que simplificaram tudo
O código legado tinha variáveis chamadas a, b, c, x, y e a função de CNPJ fazia até quatro varreduras da mesma string por chamada. A refatoração se apoiou em três decisões:
- Checar comprimento antes de qualquer processamento. Um CNPJ só pode ter 14 caracteres (sem máscara) ou 18 (com máscara). Uma string de 100 mil caracteres é rejeitada num teste O(1) — sem regex, sem cópias.
- Normalizar uma vez. No fluxo antigo, a função que identifica o documento convertia caixa, testava formato e chamava o validador específico — que repetia tudo de novo. Agora a normalização acontece uma única vez e o resultado atravessa o fluxo.
- Um laço, dois somatórios. Os dígitos verificadores de CPF e CNPJ são calculados em módulo 11 com pesos deslocados. Em vez de dois
reducecom array temporário, um únicoforacumula as duas somas.
Nomes legíveis (firstSum, secondDigit, verifierDigit) substituíram as letras soltas, e charCodeAt(i) - 48 entrou no lugar de parseInt(charAt(i)).
Uma decisão que eu destaco porque é rara em refatoração guiada por IA: não criar uma abstração genérica de “documento” pra duas funções pequenas. Complexidade de design também é custo, e a IA propôs isso por conta própria.
Como eu provei que ficou mais rápido
“Achamos que ficou mais rápido” não vale nada num post meu. Cada alegação de performance foi medida, não presumida — e o baseline foi capturado antes de tocar no código:
npm run build
cp bin/multidocument_validator.js /tmp/cnpj-before-improvements.mjs
Com o build antigo salvo, as duas versões rodam no mesmo processo, na mesma máquina, no mesmo Node, como módulos independentes. O script de benchmark descarta uma rodada de aquecimento pro JIT estabilizar, roda 7 amostras por cenário e reporta a mediana. E tem um detalhe que eu achei elegante: o resultado de cada chamada é acumulado num checksum. As duas versões produziram exatamente o mesmo valor — 3.200.000. Ou seja, a comparação de performance não esconde nenhuma mudança de comportamento por baixo dos panos.
O teste de comportamento também não ficou pra trás: 109 testes com casos-limite do módulo 11, entradas de 100 mil caracteres, até Unicode (ſ não pode virar S). E um teste que empacota a lib com npm pack e instala o tarball num diretório limpo, garantindo que um consumidor JS e outro TypeScript conseguem usar o pacote publicado de verdade.
Resultados
| Cenário | Antes (ns/chamada) | Depois | Melhoria |
|---|---|---|---|
cnpjValidator — CNPJ válido | 1.384 | 204 | −85% |
documentValidator — CNPJ válido | 1.340 | 358 | −73% |
documentValidator — CPF válido | 489 | 217 | −56% |
cpfValidator — CPF válido | 329 | 277 | −16% |
cnpjValidator — entrada de 100 mil chars | 122.866 | 6 | −99,99% |
O caso da entrada longa é o meu favorito. A versão antiga aplicava toUpperCase() e .replace() — copiando 100 KB — antes de qualquer rejeição. A nova olha o length e devolve false. Na prática, isso é o que protege sua API quando alguém cola um arquivo inteiro no campo de formulário.
E o CPF com −16%? Pois é, o ganho menor ficou explícito também. A função antiga já era razoável; a melhoria veio do laço único e do charCodeAt no lugar do parseInt. Não tudo precisa ser 85%.
As ressalvas, porque credibilidade se constrói com elas
Antes de você sair refatorando tudo, o que esse número não diz:
- Foi medido em uma máquina (Linux, Node 24, um Core Ultra 7). O benchmark é reproduzível com
npm run benchmark— rode na sua e compare. - Em nanossegundos por chamada, a diferença absoluta no dia a dia é pequena. O valor real aparece em volume: lotes, ETL, upload de planilhas, e na resistência a inputs patológicos.
- Mudou comportamento, sim — de forma intencional: dígitos excedentes no CPF agora são rejeitados, e isso está coberto por teste e documentado no CHANGELOG.
Valeu a pena?
Pra mim, o aprendizado não foi “IA escreveu minha lib”. A spec foi minha, as skills de TDD e XP eram minhas, o critério de “medir antes de mudar” veio do processo que eu já seguia. O que a IA fez foi executar esse processo numa velocidade que eu não conseguiria sozinho — e, na revisão, enxergar no meu código antigo coisas que eu tinha deixado passar por anos, inclusive um bug de validação.
Quarenta minutos. Feature nova, um bug corrigido, código mais simples e um benchmark com prova. Se você tem aquela lib sua que ninguém mexe há anos, talvez seja uma boa sessão de testes.
O benchmark é reproduzível no repositório egermano/brazilian-multidocument-validator, junto com a spec que guiou o trabalho e os dados brutos das medições.