Otimização de desempenho JavaScript: guia para iniciantes

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Introdução: por que a performance do JavaScript é importante?

Quando você escreve seu primeiro aplicativo em JavaScript, ele pode funcionar rápido e sem problemas. Mas à medida que o projeto cresce — adicionando novas funcionalidades, aumentando o volume de dados e o número de usuários — a performance começa a cair. Scripts lentos, congelamentos de interface e processamento lento de requisições são problemas típicos que até mesmo desenvolvedores experientes enfrentam.

Para programadores iniciantes, a otimização muitas vezes parece algo complexo e secundário. No entanto, entender os fundamentos da performance ajudará você a escrever não apenas código funcional, mas também código rápido desde o início. Neste artigo, vamos abordar as principais técnicas de otimização de aplicações JavaScript em Node.js que você pode aplicar ainda hoje.

1. Entendendo o Event Loop e a Assincronicidade

JavaScript é uma linguagem single-thread, mas graças ao Event Loop, ele consegue processar múltiplas tarefas sem bloqueio. Para Node.js, isso é especialmente crítico, pois o servidor precisa atender milhares de requisições simultaneamente.

O erro principal de iniciantes é usar operações síncronas onde a assincronicidade é necessária. Por exemplo, ler um arquivo grande de forma síncrona bloqueará todo o processo:

// RUIM: bloqueia o Event Loop
const fs = require('fs');
const data = fs.readFileSync('/bigfile.txt', 'utf8');
console.log(data);

Em vez disso, use métodos assíncronos:

// BOM: não bloqueia o Event Loop
const fs = require('fs');
fs.readFile('/bigfile.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

A abordagem moderna é usar async/await com promises:

// AINDA MELHOR: async/await
const fs = require('fs').promises;

async function readFile() {
  const data = await fs.readFile('/bigfile.txt', 'utf8');
  console.log(data);
}
readFile();

Dica: Sempre dê preferência às versões assíncronas das funções (com callbacks, promises ou async/await). Isso permitirá que seu servidor processe outras requisições enquanto aguarda operações de I/O.

2. Evite operações "pesadas" na thread principal

Às vezes, não há como evitar cálculos síncronos — por exemplo, criptografia, processamento de imagens ou cálculos matemáticos complexos. Se executá-los na thread principal, o aplicativo "congelará" durante a execução.

Para tais tarefas, use Worker Threads (no Node.js) ou Web Workers (no navegador). Vejamos um exemplo em Node.js:

// main.js
const { Worker } = require('worker_threads');

function runHeavyTask(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

// worker.js
const { parentPort, workerData } = require('worker_threads');
// Cálculos pesados
let result = 0;
for (let i = 0; i < 1e9; i++) {
  result += i;
}
parentPort.postMessage(result);

Essa abordagem permite manter a capacidade de resposta do aplicativo mesmo ao executar tarefas que consomem muitos recursos.

3. Otimização do trabalho com dados e cache

Frequentemente, o gargalo é a execução repetida das mesmas operações. Por exemplo, uma consulta ao banco de dados ou o cálculo de um valor complexo. A solução é o cache.

Um exemplo simples usando um objeto de cache:

const cache = {};

async function getExpensiveData(userId) {
  if (cache[userId]) {
    console.log('Retornando do cache');
    return cache[userId];
  }
  
  // Simulação de uma requisição longa
  const data = await fetchFromDatabase(userId);
  cache[userId] = data;
  return data;
}

Para cenários mais complexos, use

Blogs

Book Recommendations