Desempenho de regex: evite explosão de retrocesso

TextoText processing tools

Desempenho de regex: uma expressão pode derrubar todo o servidor

O retrocesso catastrófico (Catastrophic Backtracking) pode fazer o tempo de correspondência de uma regex passar de milissegundos para horas.

Cenário Regex Comprimento da entrada Tempo
Correspondência normal /^abc$/ 100 <1ms
Retrocesso linear /a*b/ 100 ~1ms
Retrocesso exponencial /(a+)+b/ 30 ~10s
Retrocesso exponencial /(a*)*b/ 30 ~10s

I. Princípio do mecanismo de retrocesso

Como funciona o motor NFA

JavaScript usa um motor NFA (Autômato Finito Não Determinístico) com algoritmo de retrocesso:

Entrada: "aaaac"
Regex: /(a+)+c/

Processo de correspondência:
1. (a+) corresponde "aaaa" → tenta +c → não corresponde 'c'
2. Retrocesso: (a+) corresponde "aaa" → (a+) corresponde "a" → tenta +c → não corresponde
3. Retrocesso: (a+) corresponde "aa" → (a+) corresponde "aa" → tenta +c → não corresponde
4. Retrocesso: (a+) corresponde "aa" → (a+) corresponde "a" → (a+) corresponde "a" → ...
5. ... retrocesso exponencial

Cálculo do número de retrocessos

Para /(a+)+b/ correspondendo a "aaa..." (n a's):

Número de retrocessos ≈ 2^n

n=10: ~1.024 vezes
n=20: ~1.048.576 vezes
n=30: ~1.073.741.824 vezes

II. Padrões comuns de retrocesso catastrófico

Padrão 1: Quantificadores aninhados

// Retrocesso catastrófico
/(a+)+b/     // Quantificador + aninhado
/(a*)*b/     // Quantificador * aninhado
/(\w+)+@/    // \w+ aninhado

// Correção: evitar quantificadores aninhados
/a+b/        // Quantificador de camada única
/(\w+)@/     // Quantificador de camada única

Padrão 2: Alternância ordenada

// Retrocesso catastrófico: opções de alternância com prefixo comum
/(a|ab)+c/

// Com entrada "abababab..." que não corresponde, o motor tenta todas as combinações
// Número de retrocessos ≈ O(2^n)

// Correção: extrair o prefixo comum
/a(b?)+c/

Padrão 3: Quantificadores sobrepostos

// Retrocesso catastrófico
/.*.*=/

// Ambos .* podem corresponder ao mesmo conteúdo, gerando muitos retrocessos

// Correção: combinar quantificadores
/.*=/

Padrão 4: Armadilha de referência de retrocesso

// Retrocesso catastrófico
/^(b+)(a+)\1$/

// Com entrada "bbbbaaaa", \1 testa todas as subdivisões de b+

// Correção: se possível, evitar referências de retrocesso, ou limitar o alcance do quantificador
/^(b+)(a+)\1$/.test(input) // Apenas para entradas curtas

III. Técnicas de otimização

Técnica 1: Especificar classes de caracteres

// Lento: . corresponde a qualquer caractere, muito retrocesso
/.*foo/

// Rápido: [^\n]* não corresponde a quebras de linha, reduz retrocesso
/[^\n]*foo/

// Mais rápido: se souber que antes de foo só há letras
/[a-z]*foo/

Técnica 2: Ancorar para reduzir o espaço de busca

// Lento: tenta corresponder de cada posição
/foo/

// Rápido: se foo está no início da linha
/^foo/

// Rápido: se foo está no final da linha
/foo$/

// Rápido: ancorar ambos os lados
/^foo$/

Técnica 3: Uso correto de quantificadores não greedy

// Cenário: extrair conteúdo de tags <b>

// Lento: quantificador greedy + retrocesso
/<b>(.*)<\/b>/

// Rápido: quantificador não greedy (mas ainda pode haver retrocesso)
/<b>(.*?)<\/b>/

// Mais rápido: classe de caracteres exclusiva (zero retrocesso)
/<b>([^<]*)<\/b>/
Método Correspondência <b>hello</b><b>world</b> Retrocesso
(.*) hello</b><b>world Múltiplos
(.*?) hello Poucos
([^<]*) hello Zero

Técnica 4: Grupos atômicos (ES2024+)

// Grupo atômico (?>...) uma vez correspondido, não retrocede
const regex = /^(?>a+)+b$/;

// Equivalente a: se a+ correspondeu, não retrocede para o + externo tentar novamente
// Corresponde "aaab" → sucesso
// Corresponde "aaa" → falha imediata (sem retrocesso exponencial)

Técnica 5: Quantificadores possessivos (alguns motores)

// Quantificador possessivo a++ não libera para retrocesso após corresponder
// JavaScript não suporta, mas pode ser simulado com grupos atômicos
const regex = /^(?>(a+))b$/;

Técnica 6: Pré-compilação + reutilização

// Lento: criar novo RegExp toda vez
function validate(input) {
  return /^[\w.+-]+@[\w-]+\.[\w.]+$/.test(input);
}

// Rápido: pré-compilar
const EMAIL_REGEX = /^[\w.+-]+@[\w-]+\.[\w.]+$/;
function validate(input) {
  return EMAIL_REGEX.test(input);
}

IV. Ferramentas de análise de desempenho

Usar a ferramenta de regex do ToolsKu

  1. Abrir Ferramenta de Expressões Regulares
  2. Inserir a expressão regular e o texto de teste
  3. Ver os resultados de correspondência e o tempo
  4. Ajustar a regex, comparar o desempenho antes e depois da otimização

Medição integrada no JS

function measureRegex(regex, input, iterations = 1000) {
  const start = performance.now();
  for (let i = 0; i < iterations; i++) {
    regex.test(input);
  }
  const end = performance.now();
  return {
    totalTime: end - start,
    avgTime: (end - start) / iterations,
    iterations,
  };
}

// Comparar
const slow = measureRegex(/(a+)+b/, 'a'.repeat(25));
const fast = measureRegex(/a+b/, 'a'.repeat(25));
console.log(`Lento: ${slow.avgTime}ms, Rápido: ${fast.avgTime}ms`);

Biblioteca recheck para detectar regex perigosas

npm install recheck
import { check } from 'recheck';

const result = check(/(a+)+b/);
// { safe: false, complexity: 'exponential' }

const result2 = check(/a+b/);
// { safe: true, complexity: 'linear' }

V. Comparação de desempenho de expressões regulares comuns

Regex Complexidade Segura Versão otimizada
/(a+)+b/ Exponencial ❌ /a+b/
/(a|ab)+c/ Exponencial ❌ /a(b?)+c/
/.*.*=/ Exponencial ❌ /.*=/
/(\d+){1,10}/ Polinomial ⚠️ /\d{1,10}/
/^[a-z]+$/ Linear ✅ —
/^\d{4}-\d{2}-\d{2}$/ Constante ✅ —

VI. Proteção do lado do servidor

Proteção por timeout

function safeRegexTest(regex, input, timeoutMs = 100) {
  const start = Date.now();
  const result = regex.test(input);
  if (Date.now() - start > timeoutMs) {
    throw new Error('Regex timeout: possible catastrophic backtracking');
  }
  return result;
}

Motor re2 (garantia de tempo linear)

import RE2 from 're2';

// RE2 garante tempo linear, sem explosão de retrocesso
const regex = new RE2('(a+)+b');
regex.test('a'.repeat(100)); // Seguro, não vai travar

Limitar o comprimento da entrada

function validateWithLimit(regex, input, maxLength = 1000) {
  if (input.length > maxLength) {
    throw new Error(`Input exceeds maximum length: ${maxLength}`);
  }
  return regex.test(input);
}

VII. Lista de melhores práticas

  1. Evitar quantificadores aninhados: (a+)+ → a+
  2. Especificar classes de caracteres: . → [^\n] ou classe mais específica
  3. Extrair prefixos comuns: (a|ab) → a(b?)
  4. Usar classes exclusivas em vez de não greedy: (.*?) → ([^<]*)
  5. Ancorar para reduzir o espaço de busca: ^ / $
  6. Pré-compilar expressões regulares: evitar criação repetida
  7. Usar recheck para detectar: verificar automaticamente regex perigosas no CI
  8. Proteção por timeout do servidor: prevenir DoS por regex
  9. Limitar o comprimento da entrada: reduzir o impacto do pior caso
#正则表达式#性能优化#回溯#RegExp#文本处理