PHP

Artigo 49 — Performance e Otimização Já leu

6 min de leitura

Artigo 49 — Performance e Otimização
Performance não é premature optimization — é entender onde estão os gargalos reais e eliminá-los sistematicamente. A regra 80/20 se aplica com precisão: 80%

Artigo 49 — Performance e Otimização

Módulo 10 · Semana 49 · Nível: Avançado


Introdução

Performance não é premature optimization — é entender onde estão os gargalos reais e eliminá-los sistematicamente. A regra 80/20 se aplica com precisão: 80% dos problemas de performance vêm de 20% do código, e esses 20% quase sempre são consultas ao banco de dados mal escritas ou ausência de cache.


OPcache — a otimização mais simples e impactante

O PHP, por padrão, parseia e compila cada arquivo .php em cada requisição. OPcache armazena o bytecode compilado em memória, eliminando essa etapa:

; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable                = 1
opcache.enable_cli            = 0
opcache.memory_consumption    = 256      ; MB
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.revalidate_freq       = 0       ; 0 = nunca revalida em produção
opcache.validate_timestamps   = 0       ; 0 = não verifica timestamps (produção)
opcache.save_comments         = 1       ; necessário para Doctrine/annotations
opcache.jit_buffer_size       = 100M    ; JIT — PHP 8+
opcache.jit                   = 1255    ; habilita JIT

O impacto é imediato e significativo: aplicações PHP típicas ficam entre 2x e 5x mais rápidas apenas com OPcache bem configurado.


Cache de queries com Redis

<?php
declare(strict_types=1);

class ProdutoRepositorioComCache implements ProdutoRepositorioInterface
{
    private const TTL_LISTA    = 300;  // 5 minutos
    private const TTL_PRODUTO  = 3600; // 1 hora

    public function __construct(
        private readonly ProdutoRepositorioInterface $repositorio,
        private readonly \Redis $redis,
    ) {}

    public function listarAtivos(int $pagina = 1): array
    {
        $chave = "produtos:lista:pagina:{$pagina}";

        $cached = $this->redis->get($chave);
        if ($cached !== false) {
            return json_decode($cached, associative: true);
        }

        $resultado = $this->repositorio->listarAtivos($pagina);
        $this->redis->setex($chave, self::TTL_LISTA, json_encode($resultado));

        return $resultado;
    }

    public function buscarPorId(int $id): ?array
    {
        $chave = "produtos:id:{$id}";

        $cached = $this->redis->get($chave);
        if ($cached !== false) {
            return json_decode($cached, associative: true);
        }

        $produto = $this->repositorio->buscarPorId($id);

        if ($produto !== null) {
            $this->redis->setex($chave, self::TTL_PRODUTO, json_encode($produto));
        }

        return $produto;
    }

    public function invalidarCache(int $id): void
    {
        $this->redis->del("produtos:id:{$id}");

        // Invalida todas as páginas da listagem
        $chaves = $this->redis->keys('produtos:lista:*');
        if (!empty($chaves)) {
            $this->redis->del(...$chaves);
        }
    }
}

Detectando e corrigindo o problema N+1

O N+1 é o assassino de performance mais comum em ORMs. Acontece quando você faz 1 query para buscar N registros e depois N queries para buscar dados relacionados:

<?php
// PROBLEMA: 1 query para listar 50 pedidos + 50 queries para buscar o cliente de cada um
$pedidos = Pedido::where('status', 'pendente')->get(); // 1 query

foreach ($pedidos as $pedido) {
    echo $pedido->usuario->nome; // 1 query por iteração = 50 queries!
}

// SOLUÇÃO: eager loading — traz tudo em 2 queries
$pedidos = Pedido::with('usuario')
    ->where('status', 'pendente')
    ->get(); // 2 queries: uma para pedidos, uma para todos os usuários relacionados

foreach ($pedidos as $pedido) {
    echo $pedido->usuario->nome; // sem query adicional
}

// Para detectar N+1 automaticamente no Laravel:
// composer require --dev barryvdh/laravel-debugbar
// Ou no código:
\DB::listen(function ($query) {
    if ($query->time > 100) {
        \Log::warning("Query lenta ({$query->time}ms): {$query->sql}");
    }
});

Índices de banco de dados

-- Analisar uma query lenta com EXPLAIN ANALYZE (PostgreSQL) ou EXPLAIN (MySQL)
EXPLAIN ANALYZE
SELECT p.*, c.nome AS categoria
FROM produtos p
JOIN categorias c ON c.id = p.categoria_id
WHERE p.ativo = true AND p.estoque > 0
ORDER BY p.criado_em DESC
LIMIT 15;

-- Se aparecer "Seq Scan" numa tabela grande, adicione índice:
CREATE INDEX idx_produtos_ativo_estoque ON produtos(ativo, estoque)
    WHERE ativo = true AND estoque > 0;  -- índice parcial (PostgreSQL)

CREATE INDEX idx_produtos_criado_em ON produtos(criado_em DESC);

-- Índice para busca de texto (MySQL)
ALTER TABLE produtos ADD FULLTEXT INDEX idx_fulltext_nome_descricao(nome, descricao);

-- Verificar uso de índices no MySQL
SHOW INDEX FROM produtos;
SELECT * FROM sys.schema_unused_indexes; -- índices que nunca são usados

Filas para tarefas pesadas

Nada degrada a experiência do usuário mais do que uma requisição HTTP esperando um email ser enviado, um PDF ser gerado, ou uma imagem ser processada. Mova tudo que pode esperar para fila:

<?php
// No Laravel
namespace App\Jobs;

use App\Models\Pedido;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class ProcessarPedido implements ShouldQueue
{
    use Dispatchable, Queueable;

    public int $tries   = 3;
    public int $timeout = 120;

    public function __construct(
        private readonly int $pedidoId
    ) {}

    public function handle(): void
    {
        $pedido = Pedido::findOrFail($this->pedidoId);

        // Operações pesadas aqui — não bloqueiam a resposta HTTP
        $this->processarPagamento($pedido);
        $this->notificarVendedor($pedido);
        $this->atualizarEstoque($pedido);
        $this->enviarEmailConfirmacao($pedido);
    }

    public function backoff(): array
    {
        return [30, 60, 120]; // segundos entre retentativas
    }
}

// Disparar — retorna imediatamente para o usuário
ProcessarPedido::dispatch($pedido->id);

// Com delay
ProcessarPedido::dispatch($pedido->id)->delay(now()->addMinutes(5));

Profiling com Xdebug e Blackfire

# Instalar Blackfire (profiler profissional)
curl -sS https://packages.blackfire.io/gpg.key | sudo apt-key add -
echo "deb http://packages.blackfire.io/debian any main" | sudo tee /etc/apt/sources.list.d/blackfire.list
sudo apt update && sudo apt install blackfire

# Perfilar uma requisição
blackfire curl http://localhost:8000/produtos

# Saída mostra:
# - Funções mais lentas
# - Queries mais lentas
# - Memória consumida
# - Chamadas de função e tempo em cada uma

Resumo

Técnica Impacto típico
OPcache + JIT 2–5x de melhoria geral
Cache de queries (Redis) Elimina 80–99% das queries repetidas
Eager loading Reduz N queries para 2
Índices corretos 10–1000x em queries específicas
Filas para tarefas pesadas Resposta HTTP < 200ms
Blackfire Identifica exatamente onde está o gargalo

Exercício da semana

  1. Configure OPcache e JIT no PHP. Use apache bench (ab -n 1000 -c 10 http://localhost/) para medir o impacto antes e depois.

  2. Identifique um N+1 em um projeto seu usando \DB::listen(). Corrija com with() e meça a diferença no número de queries.

  3. Adicione cache Redis ao repositório de produtos: lista com TTL de 5 minutos, produto individual com 1 hora. Implemente invalidação ao criar/atualizar/deletar.

  4. Mova o envio de email de confirmação de pedido para uma job. Garanta que o endpoint /pedidos retorna em menos de 200ms mesmo que o email demore 2 segundos.

  5. Desafio: implemente cache HTTP com ETag e Last-Modified: na rota de listagem de produtos, calcule o hash do resultado, envie como ETag. Se o cliente enviar If-None-Match com o mesmo hash, retorne 304 Not Modified sem corpo — economizando banda e processamento.


Referências

  • PHP OPcache: https://www.php.net/manual/pt_BR/book.opcache.php
  • Redis para PHP: https://github.com/phpredis/phpredis
  • Blackfire Profiler: https://www.blackfire.io/
  • Laravel Queues: https://laravel.com/docs/queues
  • Use The Index, Luke (guia de índices SQL): https://use-the-index-luke.com/

Progresso: 49/52 artigos — faltam apenas 3.

Artigos restantes: - 50 — Projeto Final Parte 1: Planejamento e Arquitetura - 51 — Projeto Final Parte 2: Implementação - 52 — Projeto Final Parte 3: Testes, Deploy e Encerramento

Continuo com o projeto final?


you asked

Sim.


claude response

Comentários

Mais em PHP

Funções
Funções

At&eacute; agora escrevemos c&oacute;digo sequencial &mdash; uma instru&ccedi...

Query Builder e Migrations
Query Builder e Migrations

No artigo anterior construímos um repositório com SQL escrito à mão. Isso fun...

Testes Automatizados com PHPUnit
Testes Automatizados com PHPUnit

Testes automatizados são o mecanismo que permite modificar código com confian...