JavaScript: Desempenho de forEach, map e reduce vs for e for...of
Tags: javascript, programming, braziliandevs
Published: 2023-01-16 17:41:22

Diversos desenvolvedores não sabem responder uma pergunta simples envolvendo o lopp `while`, `for`, etc. Muitos preferem escrever apenas código declarativo e acham mais sentido em programação imperativa. Embora concorde parcialmente com eles, isso me leva a pensar se os desenvolvedores sempre devem preferir os métodos array `.map`, `.reduce` e `.forEach` em vez de loops simples em JavaScript.

O estilo de programação declarativa é muito expressivo, mais fácil de escrever e muito mais legível. É melhor 99% das vezes, mas não quando o desempenho é importante. Os loops geralmente são três ou mais vezes mais rápidos do que suas contrapartes declarativas. 

Isso não representa uma diferença significativa na maioria das aplicações. Ainda assim, ao processar grandes quantidades de dados em alguma aplicação de inteligência de negócios, processamento de vídeo, cálculos científicos ou mecanismo de jogo, isso terá um efeito enorme no desempenho geral.

Neste artigo você verá alguns testes de desempenho com relação a esses métodos. Abaixo você verá o que abordaremos neste artigo:

- Sobre os testes
- Array.forEach vs for e for..of
- Array.map vs for vs for..of
- Array.reduce vs for e for..of
- Conclusão


## Sobre os testes

A aplicação de teste usa a biblioteca de [benchmark para obter resultados estatisticamente significativos.](https://www.npmjs.com/package/benchmark). A entrada para os testes foi um array de um milhão de objetos com a estrutura `{a: number; b: number; r: number}`. Aqui está o código que gera esse array:

```jsx
function generateTestArray() {
  const result = [];
  for (let i = 0; i < 1000000; ++i) {
    result.push({
      a: i,
      b: i / 2,
      r: 0,
    });
  }
  return result;
}
```

## Array.forEach vs for e for..of

![Operations per second, higher is better](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rwnfr3v1ciz060xl7k5a.png)

Este teste calcula a soma de `a` e `b` para cada elemento do array e o armazena em `r`:

```jsx
array.forEach((x) => {
   x.r = x.a + x.b;
});
```

Criei deliberadamente um campo `r` no objeto durante a gravação do array, para evitar alterar a estrutura do objeto, pois isso afetará os benchmarks.

Mesmo com esses testes simples, os loops são quase três vezes mais rápidos. O loop `for..of` está um pouco á frente do resto, mas a diferença não é significativa. A micro-otimização do loop `for` que funciona em algumas outras linguagens, como armazenar em cache o tamanho do array ou armazenar um elemento para acesso repetido em uma variável temporária, teve efeito zero no JavaScript em execução no V8. Provavelmente o V8 já os faz sob o capô. 

Como o loop .forEach não é diferente do loop for..of, não vemos mais sentido em usá-lo sobre o loop tradicional na maioria dos casos. Vale a pena usar apenas quando você já tiver uma função para invocar em cada elemento do array. Neste caso, é um one-liner, com degradação de desempenho zero:

```jsx
array.forEach(func);
```

## Array.map vs for vs for..of

Esse testes simples, mapeiam o array para outro array com o `a` + `b` para cada elemento:

```jsx
return array.map((x) => x.a + x.b);
```

Os loops também são muito mais rápidos aqui. O `for..of` cria um array vazio e usa um `push` em cada novo elemento:

```jsx
const result = [];
for (const { a, b } of array) {
   result.push(a + b);
}
return result;
```

Não é a abordagem ideal, pois o array é realocado dinamicamente e movido sob o capô. A versão `for` pré-aloca o array com o tamanho de destino e define cada elemento usando o índice:

```jsx
const result = new Array(array.length);
for (let i = 0; i < array.length; ++i) {
   result[i] = array[i].a + array[i].b;
}
return result;
```
Aqui, também testamos se a desestruturação tem algum efeito no desempenho. Com o `.map`, os benchmarks eram idênticos, e com `for..of`, os resultados não são tão diferentes e pode ser apenas um acaso do benchmark.

## Array.reduce vs for e for..of

Aqui, apenas calculamos a soma de `a` e `b` para todo o array:

```jsx
return array.reduce((p, x) => p + x.a + x.b, 0);
```

Ambos `for` e `for..of` são 3,5 vezes mais rápidos que reduce. No entanto, os loops são muito mais detalhados:

```jsx
let result = 0;
for (const { a, b } of array) {
   result += a + b;
}
return result;
```

Escrever tantas linhas de código para apenas uma soma simples deve ter um motivo forte, portanto, a menos que o desempenho seja tão crítico, `.reduce` é muito melhor. Os testes novamente não mostraram diferença entre os loops.

## Conclusão

Os benchmarks provaram que a programação imperativa com loops, resulta em melhor desempenho do que o usuo de métodos Array convenientes. Invocar a função callback, não é gratuito e aumenta para grandes arrays. No entanto, para um código mais complexo do que uma soma simples, não haverá muita diferença relativa, pois os próprios cálculos levariam mais tempo.

O código imperativo é muito mais detalhado na maioria dos casos. Cinco linha de código para uma sompla simples, são demais e o `.reduce` são apenas uma linha. Por outro lado, o `.forEach` é quase o mesmo que `for` ou `for..of`, apenas mais lento. Não há muita diferença de desempenho entre os dois loops e você pode usar o que for mais adequado ao algoritmo.

Ao contrário do AssemblyScript, as microotimizações do loop `for`, não fazem sentido para arrays em JavaScript. O V8 já faz um ótimo trabalho e provavelmente também elimina as verificações de limite. 

A pré-alocação de um array de comprimento conhecido, é muito mais rápido do que a depender do crescimento dinâmico com `push`. Também confirmamos que a desestruturação é gratuita e deve ser usada sempre que for conveniente. 

Bons desenvolvedores devem saber como o código funciona e escolher a melhor solução em cada situação. A programação declarativa, com sua simplicidade, vence na maioria das vezes. Escrever código de nível ionferior, só faz sentido em dois casos:

- para otimizar os gargalos encontrados por perfis extensivos
- para código de desempenho obviamente crítico

Lembre-se, otimizações prematuras são a raiz de todos os males. Obrigado por ler e espero que tenha gostado do artigo.