# 10 linhas de Node.js FFI salvam seu app

> Published 2026-10-17T01:32:07.097Z on https://www.crazystack.com.br/pt/p/10-linhas-de-node-js-ffi-salvam-seu-app/
> Source video: https://www.youtube.com/watch?v=6YfoZaHn0z4

O Node.js FFI permite substituir apenas a função que consome CPU por código nativo, sem reescrever o backend em Rust e sem compilar addons C++. Ele entrou como recurso experimental atrás de uma flag na linha 26.x e ainda é marcado como experimental, então trate-o como fronteira instável: ganho real, com risco real de travar o processo.

## O que o Node.js FFI resolve de verdade

O Node.js FFI permite que JavaScript chame funções dentro de uma biblioteca nativa compartilhada sem escrever um addon C++ e sem passar pelo node-gyp. Em vez de compilar uma ponte contra a versão exata do runtime, você aponta para o arquivo da biblioteca e descreve os tipos dos argumentos e do retorno.

Essa descrição é a ligação inteira. Não há compilador, nem configuração de build, nem aquele binding que para de carregar depois de atualizar o Node. Trata-se de uma API oficial do runtime, documentada em [node:ffi](https://nodejs.org/api/ffi.html), e ainda marcada como experimental — vale conferir o status atual na documentação antes de colocar em produção.

O ganho não é de linguagem, e sim de custo por chamada. Uma chamada síncrona leva o buffer por ponteiro, sem copiar, o que faz sentido quando você atravessa a fronteira uma vez com um trabalho grande do outro lado.

### O que o FFI não resolve

O FFI não deixa o código nativo assíncrono. Toda chamada ocupa a thread do event loop até terminar, então um resize pesado continua bloqueando as requisições seguintes durante todo o processamento. Mover o trabalho para [worker_threads](https://nodejs.org/api/worker_threads.html) é o que devolve paralelismo.

Também não substitui um addon maduro. Se já existe uma biblioteca nativa pronta para o seu caso, ela é a escolha padrão; o FFI serve justamente quando essa biblioteca não existe.

### Por que a fronteira custa caro em laço

Cada travessia paga um imposto para traduzir valores entre os dois mundos. Em uma chamada que carrega uma imagem inteira, esse custo some dentro do trabalho. Em um laço apertado que chama uma função minúscula um milhão de vezes, o imposto passa a ser a conta.

## Como saber se o gargalo é uma única função

O profiler de CPU embutido no Node mostra qual função realmente consome o tempo, e é isso que separa otimização de achismo. Você coleta alguns pedidos reais, abre o resultado como flame graph e procura a barra mais larga. Se uma única função domina o gráfico, o problema não é o framework.

O fluxo é sempre o mesmo: medir, descartar a correção barata e só então partir para código nativo. Otimizar o app inteiro sem esse passo costuma queimar dias em lugares que não explicam a latência.

### O gráfico mais plano que parece dramático

Um resize de 4 megapixels passa por três etapas. O arquivo comprimido é decodificado em mais de 4 milhões de pixels crus na memória; em seguida os pixels são reamostrados, com uma média entre vizinhos conhecida como Lanczos; por fim, o resultado é codificado em um arquivo menor.

A etapa do meio é um laço apertado executado milhões de vezes. Em JavaScript puro, esse laço passa pelo runtime a cada iteração, e é aí que o tempo se acumula. Sob 10 uploads simultâneos, a média pode continuar aceitável enquanto a cauda lenta — o que o usuário realmente sente — dispara.

### Antes de culpar a linguagem

Cache do mesmo resultado, leitura de arquivo síncrona e laço dentro de laço custam quase nada para corrigir e às vezes apagam o problema. Código nativo só faz sentido quando a carga continua pesada com o algoritmo já sadio.

## Node.js FFI contra addon nativo e reescrita

Comparar as três saídas evita o erro mais caro: tratar o FFI como upgrade universal. Cada caminho serve a um cenário distinto, e a decisão depende de já existir uma biblioteca pronta, de quantas funções estão pesadas e de quanto time você tem.

| Caminho | Quando faz sentido | O que você troca | Custo embutido |
| --- | --- | --- | --- |
| Addon nativo (por exemplo, sharp) | Já existe biblioteca madura para a tarefa | Nada do app | Depender de binários pré-compilados |
| Node.js FFI | Não existe addon para o que você precisa | Uma função | Chamada síncrona e crash de processo em caso de tipo errado |
| Reescrita em Rust ou Go | Trabalho nativo espalhado pelo app inteiro ou troca de stack por outros motivos | O backend | Meses de desenvolvimento e o risco de regressão |

O critério prático para imagens é simples. Com uma biblioteca madura disponível, use-a. Sem addon equivalente, o Node.js FFI vira a ferramenta, mirando uma biblioteca do sistema, um `.so` de fornecedor ou uma rotina C pequena que você mesmo escreveu.

### O benchmark que sustenta a escolha

O sharp, biblioteca nativa de processamento de imagens para Node, publica um benchmark em que redimensionar esse tipo de foto é cerca de 26 vezes mais rápido que a versão em JavaScript puro ([benchmark do sharp](https://sharp.pixelplumbing.com/performance)). É um número do próprio projeto, não medição independente, e ele descreve uma operação específica num conjunto de imagens específico.

A diferença vem de onde o laço roda. O sharp envolve a biblioteca C libvips e deixa o laço em instruções de máquina, em vez de atravessar o runtime a cada passo.

### O que a reescrita ainda justifica

Se o trabalho nativo está espalhado por toda a aplicação, trocar uma função não resolve nada. O mesmo vale quando o time migra para outra linguagem por razões próprias. Esses casos existem, mas são mais raros do que a proposta de reescrita sugere.

## Como chamar uma biblioteca nativa em passos

O procedimento é curto porque cada passo tem um objetivo único. A ordem importa: medir antes de trocar, trocar uma função só, mover o trabalho pesado para fora do event loop e validar na máquina de produção.

1. Rode o profiler de CPU em pedidos reais, abra o flame graph e identifique a função que domina o tempo.
2. Descarte cache, leitura síncrona e outros ajustes baratos antes de cogitar código nativo.
3. Abra a biblioteca compartilhada (`.so` no Linux, `.dylib` no macOS, `.dll` no Windows) por um handle descartável, que fecha sozinho no fim do escopo.
4. Descreva a função com os tipos de cada argumento — inteiro de 32 bits, ponteiro, tamanho — e o tipo de retorno. Essa descrição é a ligação completa.
5. Passe o ponteiro para os bytes da imagem junto com largura e altura desejadas, receba o ponteiro dos novos pixels e libere essa memória quando terminar.
6. Execute a chamada dentro de um pool de worker threads reutilizáveis, nunca criando um worker por requisição.
7. Meça o antes e o depois na mesma máquina em que você faz deploy, porque a diferença no laptop costuma enganar.

## O custo por chamada e o limite do FFI

Atravessar a fronteira entre JavaScript e código nativo não é gratuito, e é esse imposto que define quando o FFI vale a pena. Um benchmark público mediu a chamada em cerca de 37 nanossegundos, contra aproximadamente 35 nanossegundos de um addon compilado, valores que valem a pena reconferir na fonte atual.

A leitura correta é de ordem de grandeza, não de precisão. Em uma única travessia que carrega uma imagem inteira, esse custo é irrelevante diante do trabalho nativo. Em um laço quente que chama uma função minúscula milhões de vezes, o mesmo imposto se torna a conta principal.

### Onde o FFI perde

Para laço interno quente chamando função nativa pequena, um addon compilado continua vencendo. Esse é exatamente o caso do sharp, que não é um usuário de FFI e não seria beneficiado por ele. Se você já tem o addon, o FFI não é upgrade.

O sweet spot é uma chamada que carrega muito trabalho: um buffer, uma imagem, um arquivo. Você não precisa adivinhar — o runtime traz ferramentas de medição embutidas para comparar o antes e o depois na sua própria carga.

### O risco que não aparece no gráfico

Carregar uma biblioteca nativa executa o código dela dentro do seu processo. Trate o arquivo `.so` como qualquer dependência que você distribui: saiba de onde veio e restrinja o uso em produção com a flag de permissão.

A própria documentação avisa que essa API é insegura. Descrever os tipos errados ou passar um ponteiro inválido não gera erro limpo, e sim o encerramento do processo inteiro. Encapsule a chamada em um módulo pequeno, teste com rigor e mantenha o resto do app longe da fronteira crua.

## Por que worker threads decidem o resultado

O Node roda o JavaScript em uma thread, o event loop, que atende milhares de requisições alternando entre o que está pronto. Uma chamada nativa síncrona ocupa essa thread por completo até terminar, então as requisições seguintes ficam paradas na fila.

A correção é mover o trabalho pesado para fora da thread principal. Um worker é uma thread separada com seu próprio JavaScript: você entrega a imagem, ele roda o resize lá e devolve o resultado. O buffer pode ser transferido em vez de copiado, o que mantém a troca barata.

### Pool em vez de worker por requisição

Criar um worker a cada requisição destrói o ganho. Mantenha um pool pequeno e reutilize. Com 10 requisições em um pool de quatro workers, quatro rodam ao mesmo tempo e as mesmas 10 terminam em uma fração do tempo de parede, enquanto a thread principal continua atendendo.

O paralelismo real para no número de núcleos disponíveis. Um laptop esconde essa limitação atrás de oito núcleos; um servidor barato de dois núcleos não esconde nada.

### Onde medir a diferença

Meça na máquina em que você faz deploy, não na de desenvolvimento. A diferença entre o antes e o depois costuma ser maior em produção, porque é lá que a fila do event loop realmente se forma sob carga.

## Perguntas frequentes sobre Node.js FFI

- **O Node.js FFI substitui um addon nativo como o sharp?**

Não. O FFI serve quando não existe addon para a tarefa. Se já há biblioteca nativa madura, ela continua sendo a escolha padrão, e o FFI não é upgrade sobre ela.

- **A chamada nativa bloqueia o event loop?**

Sim. A chamada é síncrona e ocupa a thread principal até terminar. Para trabalho pesado, rode dentro de um pool de worker threads reutilizáveis para não travar as demais requisições.

- **O FFI vale a pena em laço apertado?**

Não. Cada travessia paga um imposto pequeno, que vira a conta principal quando você chama uma função minúscula milhões de vezes. Nesse caso, um addon compilado vence.

- **É seguro usar esse recurso em produção?**

É uma API experimental e a documentação a descreve como insegura: tipos errados ou ponteiro inválido derrubam o processo. Encapsule a chamada, teste bem e restrinja o carregamento por permissão.

- **Preciso reescrever o backend para ganhar desempenho?**

Na maioria dos casos, não. Substituir a única função que queima CPU, mantendo rotas, middleware e banco intactos, resolve a cauda lenta sob carga.

[Source video](https://www.youtube.com/watch?v=6YfoZaHn0z4)
