Por ··9 min read

pnpm por dentro: arquitetura, melhorias e por que usar

Uma análise técnica do store endereçado por conteúdo, node_modules estrito, workspaces, segurança da cadeia de suprimentos e migração para pnpm.

Trocar npm install por pnpm install parece uma otimização pequena. Não é. O pnpm muda a forma como dependências são armazenadas, ligadas ao projeto e validadas. O resultado costuma ser menos uso de disco, instalações repetidas mais rápidas e uma classe inteira de erros descoberta antes do deploy.

A melhor razão para adotar pnpm não é velocidade isolada. É fazer o node_modules representar com mais fidelidade o grafo declarado no package.json.

O problema que o pnpm ataca

Gerenciadores tradicionais tendem a produzir uma árvore achatada em node_modules. O hoisting move pacotes transitivos para níveis superiores, onde o código da aplicação também consegue importá-los.

Imagine que a aplicação importe kleur, mas somente vitest declare essa dependência. O projeto pode funcionar hoje porque kleur foi elevado até a raiz. Uma atualização do Vitest pode alterar a árvore, remover esse acesso acidental e quebrar a aplicação sem que seu código tenha mudado.

Essa é uma dependência fantasma: o código usa um pacote que não declarou. O pnpm reduz esse problema ao expor na raiz apenas as dependências diretas do projeto. Se o import não está no package.json, a falha aparece cedo.

Como o armazenamento funciona

O pnpm separa a instalação em duas ideias: um store global endereçado por conteúdo e uma estrutura local que materializa o grafo do projeto.

1. Store endereçado por conteúdo

Os arquivos baixados são identificados pelo hash do próprio conteúdo e ficam em um store compartilhado pela máquina. Se dez projetos usam exatamente o mesmo arquivo, ele precisa ser armazenado uma vez.

Quando uma nova versão de um pacote muda apenas parte dos arquivos, somente o conteúdo novo precisa ser acrescentado ao store. Isso é mais granular do que manter uma cópia completa de cada pacote em cada projeto.

Você pode localizar e inspecionar esse store com:

pnpm store path
pnpm store status
pnpm store prune

prune deve ser usado como manutenção eventual, não depois de toda instalação: remover versões ainda úteis elimina parte do ganho das instalações futuras.

2. Virtual store e links

Dentro do projeto, o diretório node_modules/.pnpm funciona como um virtual store. Os arquivos dos pacotes são ligados ao store global com hard links. Em seguida, links simbólicos montam as relações entre dependências.

Uma versão simplificada fica assim:

node_modules/
├── react -> .pnpm/react@19.0.0/node_modules/react
└── .pnpm/
    ├── react@19.0.0/
    │   └── node_modules/react
    └── scheduler@0.25.0/
        └── node_modules/scheduler

A profundidade física permanece praticamente constante mesmo quando o grafo lógico cresce. Isso evita os caminhos gigantes de uma árvore ingenuamente aninhada e continua compatível com a resolução de módulos do Node.js, que considera o caminho real por trás do link simbólico.

Peer dependencies também entram na identidade do pacote dentro do virtual store. Duas instâncias da mesma biblioteca podem coexistir quando cada uma foi resolvida contra peers diferentes, sem fingir que representam o mesmo ambiente.

Por que instalações repetidas ficam rápidas

O processo é dividido em resolução, busca e ligação. Essas etapas podem avançar com sobreposição, e pacotes já presentes no store não precisam ser baixados de novo. Na prática, o maior benefício aparece em máquinas com vários projetos, worktrees e branches que compartilham boa parte do grafo.

Isso não significa que o pnpm vencerá todo benchmark. Uma instalação fria ainda depende de rede, latência do registry, antivírus, sistema de arquivos e scripts nativos. Em CI efêmero, restaurar um cache enorme também pode custar mais do que baixar os pacotes. A comparação correta usa o repositório e a infraestrutura da equipe, medindo instalação fria, instalação com store aquecido e tamanho do cache.

As melhorias que vão além da performance

Workspaces como recurso central

O suporte a monorepos é nativo. Um pnpm-workspace.yaml define os projetos e um único lockfile pode representar todo o workspace:

packages:
  - "apps/*"
  - "packages/*"

O protocolo workspace: garante que uma dependência interna seja resolvida localmente. Se a versão esperada não existir no repositório, a instalação falha em vez de buscar silenciosamente um pacote homônimo no registry.

{
  "dependencies": {
    "@acme/design-system": "workspace:^"
  }
}

Filtros permitem executar uma tarefa em uma parte do grafo. Os três pontos incluem também as dependências do pacote selecionado:

pnpm --filter @acme/web... build
pnpm --filter "...[origin/main]" test

Isso não substitui ferramentas de cache de tarefas como Turborepo ou Nx, mas oferece a base correta para elas: seleção de pacotes, ordem topológica e dependências locais explícitas.

Catálogos de versões

Em um monorepo, repetir a versão de React, TypeScript ou ESLint em dezenas de arquivos aumenta o custo de atualização. Os catálogos centralizam essas faixas:

catalog:
  react: ^19.0.0
  react-dom: ^19.0.0
  typescript: ^5.7.0
{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  }
}

Na publicação de um pacote, o protocolo catalog: é convertido para uma faixa semver normal. O consumidor não precisa usar pnpm. Para equipes grandes, isso reduz versões divergentes, alterações repetitivas e conflitos de merge.

Segurança por padrão

Scripts de instalação de dependências são uma superfície crítica: um postinstall malicioso executa código durante o simples ato de instalar. Desde a versão 10, o pnpm não executa automaticamente esses scripts em dependências. Versões atuais permitem manter uma lista explícita de pacotes autorizados:

allowBuilds:
  esbuild: true
  core-js: false

Há outras políticas úteis no pnpm-workspace.yaml:

# Aguarda 24 horas antes de resolver uma versão recém-publicada.
minimumReleaseAge: 1440

# Impede dependências transitivas vindas de Git ou URLs arbitrárias.
blockExoticSubdeps: true

# Falha quando a evidência de confiança piora entre publicações.
trustPolicy: no-downgrade

Na versão 11, a espera de 24 horas já é o padrão. Ela reduz a janela de exposição entre a publicação de um pacote comprometido e sua detecção pelo ecossistema. Não elimina o risco: lockfile revisado, registry confiável, auditoria e menor número de dependências continuam necessários.

Lockfile e CI mais previsíveis

O pnpm-lock.yaml registra resoluções e integridade do grafo. Em ambiente de CI, o pnpm ativa o comportamento de lockfile congelado automaticamente. Ainda assim, vale deixar a intenção explícita no pipeline:

pnpm install --frozen-lockfile
pnpm test
pnpm build

Fixe também a versão do gerenciador no package.json. Use a versão exata adotada pela equipe para evitar que uma mudança de major reescreva o lockfile:

{
  "packageManager": "pnpm@11.22.0"
}

Docker com cache mais estável

pnpm fetch baixa o grafo usando o lockfile, sem depender dos manifests de cada pacote. Isso cria uma camada Docker que só é invalidada quando as dependências mudam:

COPY pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm fetch --prod

COPY . .
RUN pnpm install --offline --frozen-lockfile --prod

Em monorepos, essa abordagem evita copiar manualmente cada package.json antes da instalação. Hard links não atravessam o limite entre host e container durante o build; nesse cenário, cache mounts do BuildKit ou camadas baseadas no lockfile são as otimizações relevantes.

O que melhorou nas versões recentes

O pnpm deixou de ser apenas o gerenciador "rápido e econômico". A evolução das versões 10 e 11 adicionou controles de cadeia de suprimentos, catálogos, aprovação granular de builds, políticas de confiança e configuração central no pnpm-workspace.yaml.

No momento desta publicação, a versão 12 está em release candidate e reescreve o executável em Rust. A distribuição passa a ser um binário nativo e o uso do gerenciador permanece essencialmente igual. É uma direção técnica promissora, mas não é motivo para colocar um RC em produção sem validar compatibilidade. A arquitetura de store, links, lockfile e workspaces já entrega os benefícios na versão estável.

Limites e custos da adoção

O modelo é melhor, mas não gratuito:

  • Ferramentas antigas podem assumir um node_modules totalmente achatado ou não lidar bem com symlinks. nodeLinker: hoisted existe como escape, com perda de parte da rigidez.
  • A primeira migração costuma revelar dependências fantasmas. Isso é trabalho adicional, embora seja exatamente o tipo de problema que convém corrigir.
  • Bibliotecas que precisam de postinstall, como binários nativos, exigem aprovação explícita. A equipe precisa revisar a lista, não liberar tudo.
  • Em CI descartável, o store só ajuda entre jobs se houver uma estratégia de cache. O cache deve ser gravável apenas por jobs confiáveis.
  • pnpm gerencia o workspace, mas não resolve sozinho versionamento e changelog de múltiplos pacotes. Changesets ou outra ferramenta continuam necessários.

Migração segura a partir do npm

Para um projeto existente, faça a troca em uma branch isolada:

# Instala o pnpm atual.
npx get-pnpm

# Converte o package-lock.json para pnpm-lock.yaml.
pnpm import

# Instala e valida o novo grafo.
pnpm install
pnpm test
pnpm build

Depois:

  1. Corrija imports de dependências não declaradas em vez de ativar hoisting global imediatamente.
  2. Revise avisos de peer dependencies e scripts de build bloqueados.
  3. Fixe a versão no campo packageManager.
  4. Remova o package-lock.json somente após revisar e versionar o pnpm-lock.yaml.
  5. Troque os comandos da CI por pnpm install --frozen-lockfile.
  6. Meça tempo e cache antes e depois; não dependa de benchmarks genéricos.

Quando vale a pena usar

Eu adotaria pnpm como padrão em projetos novos e, principalmente, quando houver:

  • vários projetos Node.js na mesma máquina;
  • monorepo com pacotes internos;
  • CI ou builds Docker sensíveis a reinstalações;
  • problemas recorrentes com dependências fantasmas;
  • necessidade de controlar scripts de instalação e risco de supply chain.

Para um repositório pequeno, estável e sem gargalo de instalação, a migração não é urgente. npm e Yarn também evoluíram e podem ser escolhas válidas. Mas, quando o grafo cresce, o pnpm combina eficiência de armazenamento, correção estrutural e políticas operacionais em uma ferramenta única. É esse conjunto, e não apenas alguns segundos a menos no install, que justifica a adoção.

Referências

Leia também