Performance

Performance e Core Web Vitals no Magento 2: Guia Técnico Completo

Da compilação de produção ao Varnish com ESI, Redis/Valkey, OPcache, OpenSearch e o fluxo de diagnóstico campo→lab→APM→profiler: o roteiro denso, com env.php, my.cnf e php.ini reais, que uso para deixar lojas Magento 2 e Adobe Commerce rápidas e aprovadas nos Core Web Vitals.

Por Roger Takemiya · Atualizado em 14 de junho de 2026 · 28 min de leitura

Loja Magento 2 lenta não é destino: na enorme maioria dos casos que atendo, é resultado de configuração incompleta, tuning que parou no setup e nunca chegou ao tuning, ou uma stack mal dimensionada. Quando o production mode, o Varnish (com ESI), o Redis (ou Valkey, nas versões recentes), o OPcache e o OpenSearch estão corretamente alinhados, a mesma loja que carregava em segundos passa a servir páginas em cache direto da memória, com TTFB de dezenas de milissegundos.

Desde 12 de março de 2024, o Google mede a experiência real do usuário com três métricas — LCP, INP e CLS, os Core Web Vitals — e elas influenciam ranqueamento e, principalmente, conversão. Este guia não é mais um post de "configure uma vez": cada seção traz o comando exato, o trecho de config real (env.php, my.cnf, php.ini, VCL, Nginx), o gotcha que derruba quem só copia documentação, e o fluxo de diagnóstico que separa problema de servidor de problema de navegador.

O conteúdo serve tanto para quem roda Magento Open Source em servidor próprio quanto para quem está em Adobe Commerce Cloud com Fastly. Vamos do banco ao navegador, sem deixar buracos. Performance e visibilidade andam juntas: tudo o que você lê aqui se conecta com o SEO Técnico para Magento 2, já que velocidade de página é sinal de ranqueamento.

Production mode por dentro: SCD com -j, di:compile e o que vai para generated/

O primeiro e mais barato ganho de performance em qualquer loja Magento 2 é rodar no modo de produção. No modo developer ou default, o framework gera classes sob demanda a cada request, escreve logs verbosos e não usa código pré-compilado — aceitável para desenvolver, fatal em produção. Mas "rodar production mode" não é uma caixa-preta: vale entender o que cada etapa gera e como acelerá-la.

Os comandos e o que cada um faz

bin/magento deploy:mode:show
bin/magento deploy:mode:set production

Ao executar deploy:mode:set production, o Magento dispara em sequência a compilação de DI (setup:di:compile) e o deploy de conteúdo estático (setup:static-content:deploy). Segundo a documentação da Adobe, o modo produção tem melhor performance porque os static view files ficam pré-populados em pub/static e por causa da compilação de código. (Adobe — Set the operation mode)

O que setup:di:compile realmente gera

Aqui mora um mal-entendido comum: a compilação não "otimiza o PHP". Ela pré-gera código que, no modo developer, seria escrito em runtime. Tudo vai para generated/code e generated/metadata:

  • Proxies: classes que adiam a instanciação de dependências pesadas até o primeiro uso real.
  • Factories: as ...Factory que você injeta para criar instâncias de models/entidades.
  • Interceptors (plugins): as classes que materializam os before/around/after dos plugins declarados em di.xml.
  • Repositories e extension attributes: implementações geradas a partir das interfaces de service contract.

Por que isso acelera? Porque, sem compilar, o Magento teria que gerar essas classes na primeira requisição que as exige — I/O de disco, reflexão e escrita no meio do fluxo do usuário. Pré-gerar elimina isso. O gotcha clássico: editou di.xml ou criou um plugin e "não pegou"? Você esqueceu o setup:di:compile (em produção) ou tem um generated/ velho.

A flag que mais reduz tempo de build: -j (jobs)

Esse é o gotcha de build que separa quem conhece a ferramenta de quem só copia o comando da doc. Por padrão o static-content:deploy roda praticamente single-thread; servidores de build com 8-16 núcleos ficam ociosos. A flag --jobs / -j paraleliza a geração por tema/locale:

# paraleliza usando todos os núcleos disponíveis
bin/magento setup:static-content:deploy pt_BR en_US -j $(nproc)

# ou fixe um número (ex.: 4) para não saturar I/O
bin/magento setup:static-content:deploy pt_BR en_US -j 4

Na prática, em catálogos com 2-3 temas e 2 locales, vi o build cair de minutos para um terço do tempo só com -j. Dois alertas de campo: (1) ganho satura por volta de 8 jobs por causa de I/O; (2) se a geração roda em paralelo competindo na mesma conexão Redis, você pode ver erros de unserialize — nesse caso force -j 1 nesse passo específico. (referência de build, Sam James)

Deploy por tema e por área — pare de gerar o que não usa

Se sua loja usa só um tema de frontend, não há por que gerar Luma inteiro. Restrinja por tema (--theme) e por área (-a frontend / -a adminhtml):

bin/magento setup:static-content:deploy pt_BR \
  --theme Vendor/custom --theme Magento/blank \
  -a frontend -j $(nproc)

Para invalidar o cache de assets em cluster/CDN sem trocar o conteúdo, use --content-version (mesmo valor em todos os nós para que o caminho static/version{N}/ bata em todas as máquinas).

quick vs standard vs compact: o trade-off real

Estratégia (-s)O que fazQuando uso
quick (padrão)Minimiza o tempo de build; pode deixar resíduo de geração sob demandaLoja com 1 tema/1 locale, deploy rápido, dev/staging
standardGera TODOS os arquivos de todos os pacotes/temas/locales — build mais longo, porém previsívelProdução com múltiplos temas/locales, onde quero zero surpresa
compactSobrescreve duplicatas de forma simbólica para economizar discoServidores com disco apertado; custa um pouco de CPU em runtime ao resolver os symlinks
bin/magento setup:static-content:deploy pt_BR en_US -s standard -j 4

Regra que sigo: standard em produção multi-tema; quick só onde simplicidade e tempo de build importam mais que previsibilidade.

Cloud: build é build, deploy é deploy

No Adobe Commerce Cloud, o static content deve ser gerado na fase de build, nunca na de deploy — o hook de build já roda composer install e setup:di:compile. Gerar no deploy infla o downtime. Se você está chegando do legado, alinhe essa rotina desde o início; trato disso no checklist de corte da migração de Magento 1 para Magento 2.

OPcache e PHP-FPM: o tuning de runtime que quase ninguém faz

Production mode pré-compila DI e static content, mas o bytecode PHP ainda precisa ser interpretado a cada request — a menos que o OPcache esteja bem configurado. Em loja real, um OPcache mal dimensionado é a diferença entre TTFB de 80 ms e de 400 ms num MISS de cache. Esta dimensão estava completamente ausente em muitos guias; é das que mais movem o ponteiro.

php.ini de produção que uso

; --- OPcache ---
opcache.enable=1
opcache.memory_consumption=512        ; MB; Magento é grande, 256 estoura
opcache.interned_strings_buffer=32     ; MB
opcache.max_accelerated_files=130000   ; Magento tem >100k arquivos PHP
opcache.validate_timestamps=0          ; produção: NÃO checa mtime
opcache.save_comments=1                ; OBRIGATÓRIO no Magento (anotações)
opcache.enable_cli=1

; --- realpath cache (resolução de paths) ---
realpath_cache_size=10M
realpath_cache_ttl=86400

; --- runtime ---
memory_limit=2G                        ; CLI/compile pode exigir mais
max_execution_time=18000               ; só para CLI longo

Por que cada um importa:

  • memory_consumption ≥ 512: o Magento tem milhares de classes; com pouca memória o OPcache começa a despejar e reaquecer, matando o ganho.
  • max_accelerated_files ≥ 100000: uma instalação típica passa de 100 mil arquivos PHP; abaixo disso, parte fica fora do cache e é reinterpretada.
  • save_comments=1: o Magento lê anotações em docblocks; desligar isso quebra o framework. Nunca desligue.

O gotcha de validate_timestamps=0: resetar no deploy

Com validate_timestamps=0, o OPcache nunca verifica se o arquivo mudou no disco — ótimo para performance, mas significa que, depois de subir código novo, o PHP continua servindo o bytecode antigo até você esvaziar o OPcache. Por isso todo deploy precisa resetar o OPcache recarregando o PHP-FPM:

sudo systemctl reload php8.3-fpm   # ou o opcache_reset() via web/CLI

Esquecer esse passo é a causa de "subi o fix e nada mudou" mais comum que vejo em servidor próprio.

OPcache preload (PHP 7.4+)

O preload carrega classes na memória compartilhada no start do PHP-FPM, eliminando o custo de carregá-las no primeiro request. O Magento gera um script de preload em generated/; aponte o php.ini para ele:

opcache.preload=/var/www/magento/generated/preload.php
opcache.preload_user=www-data

Atenção: como o preload fixa o estado das classes no start, qualquer mudança de código exige restart (não apenas reload) do FPM para repopular. Em loja com deploys frequentes, avalie se o ganho compensa o restart.

PHP-FPM: dimensione pm.max_children por memória

O erro mais comum é deixar pm.max_children alto demais e o servidor entrar em swap sob carga. A conta é simples: RAM disponível para PHP ÷ memória média por worker. Um worker Magento consome facilmente 80-150 MB.

; www.conf
pm = dynamic                  ; static só se a carga for muito estável
pm.max_children = 40          ; ex.: ~6GB livres / ~150MB por worker
pm.start_servers = 8
pm.min_spare_servers = 6
pm.max_spare_servers = 12
pm.max_requests = 500         ; recicla worker p/ evitar leak de memória

Quando vejo 502/504 sob pico, a primeira coisa que checo é se o número de children bate com a RAM e se não há swap; pm = static com max_children calibrado costuma dar o menor TTFB em servidores dedicados.

Full Page Cache: Varnish, ESI, grace mode e como medir hit ratio de verdade

O Full Page Cache (FPC) guarda a página HTML inteira em cache, evitando que o PHP reprocesse o catálogo a cada visita. O Magento traz um FPC built-in, mas a Adobe é categórica: em produção, use Varnish. Segundo a documentação, a Adobe recomenda fortemente o Varnish em produção por ser significativamente mais rápido que o full-page cache nativo. (Adobe — Caching overview)

Quando o built-in FPC basta (e quando não)

  • Built-in FPC (em Redis): suficiente para dev/staging, baixo tráfego ou quando você não tem acesso root para subir Varnish. Já é rápido por estar em memória (Redis), mas a página ainda passa pelo PHP/Nginx.
  • Varnish: mandatório em produção com tráfego. Ele serve a resposta da RAM antes de o request tocar PHP, e habilita ESI, grace mode e shielding — recursos que o built-in não tem.

Ativando e configurando

Pelo Admin: Stores > Configuration > Advanced > System > Full Page Cache > Varnish Caching. Pela CLI, valor 2 ativa o Varnish (1 é o built-in):

bin/magento config:set --scope=default --scope-code=0 \
  system/full_page_cache/caching_application 2

Campos: Access list, Backend host, Backend port e Grace period. O default do Varnish no Magento é TTL 86400s e grace 300s.

Exporte o VCL — não edite no escuro

O VCL é gerado pelo Magento; gere-o via CLI e versione-o no repositório (no Fastly, isso vira custom VCL):

bin/magento varnish:vcl:generate \
  --export-version=6 \
  --backend-host=localhost --backend-port=8080 \
  --access-list=127.0.0.1 --grace-period=300 \
  --output-file=default.vcl

O arquivo gerado contém a lógica de cache do Magento (regras de bypass por cookie, ESI, health probe). É nele que você ajusta hosts de cache e regras custom. (Adobe — Configure Varnish for Commerce)

Grace mode de verdade (não é "servir stale se o backend cai")

Grace mode é mais sofisticado do que normalmente se descreve: ele desacopla o TTL de frescor do TTL de tolerância. Quando um objeto expira, em vez de fazer o usuário esperar a revalidação, o Varnish serve a versão stale imediatamente E revalida assincronamente com o backend em background (beresp.grace). O usuário nunca paga o custo do MISS; o próximo request já recebe a versão fresca. Isso protege o backend de "thundering herd" quando um conteúdo popular expira. Health probe e saint mode completam o quadro, marcando o backend como sick e estendendo a tolerância quando ele está instável.

ESI: o segredo de servir página cacheada COM conteúdo dinâmico

Este é o ponto mais sofisticado de Varnish + Magento e que costuma faltar. Como servir uma página em cache que mostra mini-cart, mensagem de boas-vindas e wishlist personalizados? Há dois mecanismos:

  • Padrão (private content via JS): a página inteira é pública/cacheável; os blocos privados são preenchidos por Ajax em /customer/section/load, controlados pelo cookie private_content_version. O Varnish serve a casca cacheada e o navegador hidrata o que é do cliente — explico isso em detalhe na seção de private content.
  • ESI (Edge Side Includes): blocos marcados com ttl próprio viram <esi:include>; o Varnish monta a página juntando o fragmento (com seu TTL) ao corpo cacheado. (Adobe — Varnish ESI block)

Medir hit ratio: uma página vs o agregado

Inspecionar HIT/MISS de UMA página é só o começo:

curl -sI https://sua-loja.com.br/categoria.html | grep -i \
  -e x-magento-cache-debug -e age -e set-cookie

O que importa em produção é o hit ratio agregado. Use o varnishstat e calcule hit / (hit + miss):

varnishstat -1 -f MAIN.cache_hit,MAIN.cache_miss
# hit ratio = cache_hit / (cache_hit + cache_miss)

Para achar o que fura o cache: varnishtop -i ReqURL (URLs mais batidas) e varnishhist (distribuição de tempo de resposta — picos longos = MISS frequentes). Na minha experiência, a maior causa de hit ratio baixo é Set-Cookie em página pública (módulo de terceiros mal-comportado): qualquer Set-Cookie torna a resposta privada e a tira do cache.

Customer data, sections e private content: por que a página cacheia mesmo com mini-cart

Esta é a peça que explica o "como" por trás de tudo que falamos no Varnish, e que costuma ficar sem resposta: como uma página servida da RAM do Varnish mostra o mini-cart, a wishlist e o "Olá, Fulano" personalizados? Entender isso resolve metade dos bugs de "carrinho não atualiza" e "preço de cliente B2B aparece para todo mundo".

O modelo: casca pública + ilhas privadas via Ajax

O Magento divide o conteúdo em público (catálogo, layout, blocos de CMS — cacheável no Varnish) e privado (carrinho, dados do cliente, comparações). A página inteira é pública e cacheada; os pedaços privados são injetados no navegador pela biblioteca Magento_Customer/js/customer-data, que:

  • guarda os dados privados em localStorage (rápido, sem ir ao servidor a cada navegação);
  • invalida seções segundo regras declaradas em sections.xml (ex.: adicionar ao carrinho invalida a seção cart);
  • busca o que está "sujo" em GET /customer/section/load?sections=cart,customer.

O cookie private_content_version

Quando uma ação muda dados do cliente, o Magento incrementa o cookie private_content_version. O JS compara a versão do cookie com a do localStorage; se diferem, dispara o section/load para ressincronizar só as seções afetadas. É por isso que a página fica cacheável: o conteúdo privado vive fora do HTML cacheado. (Adobe — Private content)

O elo com o Varnish: por que Set-Cookie fura cache

Agora fecha o raciocínio da seção anterior. O Varnish só cacheia respostas sem dados de sessão. Se um módulo seta um Set-Cookie (ou marca o bloco como private) numa página de catálogo, o Varnish entende que aquela resposta é específica de um usuário e não a cacheia — hit ratio despenca. O design correto do Magento é: catálogo nunca toca sessão; tudo que é do cliente vai pelo canal customer-data/section/load.

Como diagnosticar em campo

# o section/load deve responder rápido e SÓ as seções pedidas
curl -s 'https://sua-loja.com.br/customer/section/load?sections=cart' \
  -H 'Cookie: PHPSESSID=...' | head

# no DevTools: localStorage tem 'mage-cache-storage' com as seções
# e a request /section/load aparece após add-to-cart

Quando o mini-cart "não atualiza", quase sempre é (a) sections.xml não invalidando a seção certa, ou (b) cron/Varnish servindo o cookie de versão errado. E se uma seção custom faz uma query pesada, o section/load vira gargalo de Ajax em todas as páginas — meça-o como mediria qualquer endpoint.

Redis/Valkey: env.php real, maxmemory-policy por papel, session locking e compressão

O Redis (ou Valkey) tira do disco/banco o armazenamento de cache e sessões, colocando tudo em memória. No Magento ele atende três funções: default cache (backend), page cache (FPC) e session storage. Porta padrão 6379. O problema dos guias rasos é parar no CLI; quem debuga precisa ver o env.php real.

Configuração via CLI (gera o env.php)

bin/magento setup:config:set \
  --cache-backend=redis --cache-backend-redis-server=127.0.0.1 \
  --cache-backend-redis-db=0 \
  --cache-backend-redis-password='SENHA'

bin/magento setup:config:set \
  --page-cache=redis --page-cache-redis-server=127.0.0.1 \
  --page-cache-redis-db=1

bin/magento setup:config:set \
  --session-save=redis --session-save-redis-host=127.0.0.1 \
  --session-save-redis-db=2 --session-save-redis-max-concurrency=20

Prefiro o CLI a editar o env.php na mão: ele valida antes de gravar. Mas é o env.php que você lê quando algo quebra.

Trecho real de app/etc/env.php

'cache' => [
    'frontend' => [
        'default' => [
            'backend' => 'Magento\\Framework\\Cache\\Backend\\Redis',
            'backend_options' => [
                'server' => '127.0.0.1', 'port' => '6379', 'database' => '0',
                'password' => 'SENHA',
                'compress_data' => '1', 'compression_lib' => 'zstd',
            ],
        ],
        'page_cache' => [
            'backend' => 'Magento\\Framework\\Cache\\Backend\\Redis',
            'backend_options' => [
                'server' => '127.0.0.1', 'port' => '6379', 'database' => '1',
                'compress_data' => '0',  // FPC já comprime pouco; medir
            ],
        ],
    ],
],
'session' => [
    'save' => 'redis',
    'redis' => [
        'host' => '127.0.0.1', 'port' => '6379', 'database' => '2',
        'password' => 'SENHA',
        'disable_locking' => '0',
        'max_concurrency' => '20',
        'break_after_frontend' => '5',
        'break_after_adminhtml' => '30',
        'bot_lock_timeout' => '30',
        'compression_threshold' => '2048',
        'compression_library' => 'zstd',
    ],
],

Repare nas três bases distintas (0/1/2). Segundo a Adobe, se mais de um tipo de cache usa Redis, os números de banco precisam ser diferentes; senão um cache:flush no FPC apagaria também as sessões, esvaziando carrinhos. (Adobe — Use Redis for the page cache)

maxmemory-policy POR PAPEL — o gotcha que desloga clientes

Esse é um erro grave e silencioso. A política de despejo do Redis deve ser diferente para cache e para sessão:

Instânciamaxmemory-policyPor quê
default cache / page_cacheallkeys-lruCache é descartável; despejar o menos usado é o comportamento desejado
sessionnoeviction ou volatile-lruSessão NÃO pode ser despejada por LRU; com allkeys-lru, sob pressão de memória o Redis expulsa sessões ativas e desloga clientes no meio do checkout

Por isso eu uso instâncias Redis separadas (não só DBs diferentes) para sessão e cache em lojas de porte: cada uma com sua maxmemory-policy, e um pico de FPC nunca compete por memória com as sessões.

Session locking: a fonte de timeout no checkout

O Magento usa locking no Redis para evitar corrida entre requests concorrentes da mesma sessão. Mal configurado, ele engasga o checkout, onde várias chamadas Ajax (customer-data, estimativas) disputam a mesma sessão:

  • max_concurrency (padrão 6): número de tentativas concorrentes antes de erro. Em checkout com muito Ajax, 6 é baixo — subo para 20.
  • disable_locking: manter 0 (ligado) por segurança; desligar só com pleno conhecimento das corridas que pode causar.
  • break_after_* / bot_lock_timeout: quanto tempo um request espera o lock; bot_lock_timeout evita que bots segurem locks indefinidamente.

Quando vejo requests /customer/section/load presas em fila no waterfall durante o checkout, o suspeito número um é max_concurrency baixo.

Compressão

O Magento comprime valores acima de compression_threshold (padrão 2048 bytes). Nas versões novas, prefira zstd ou lz4 (melhor relação CPU/compressão) a gzip. Para serialização, igbinary reduz tamanho e CPU. (Cm_RedisSession — referência do handler)

Valkey: o caminho de longo prazo

Na página oficial de System Requirements, a linha 2.4.7-p10 já lista o Redis como não suportado e indica Valkey 8.1; a partir do 2.4.8 o Valkey 8 entra como alternativa compatível (mesmos flags de CLI, mesmo env.php) e no 2.4.9 ele substitui o Redis na própria ferramenta de linha de comando. O Valkey é um fork open-source do Redis mantido pela Linux Foundation, criado após a mudança de licença do Redis em 2024. Em ambientes gerenciados, use --cache-backend-redis-password e TLS/ACL. Os comandos acima seguem válidos no 2.4.8.

OpenSearch/Elasticsearch: configurar, testar, tunar JVM e operar índices

Desde o Magento 2.4.0 o MySQL como motor de busca foi removido e, desde o 2.4.4, toda instalação obrigatoriamente usa Elasticsearch ou OpenSearch. Segundo a Adobe, todas as instalações 2.4 devem usar Elasticsearch ou OpenSearch como solução de busca de catálogo, e o suporte a OpenSearch foi adicionado no 2.4.4. (Adobe — Search engine overview)

Qual escolher

Para projetos novos, recomendo OpenSearch: o fim de suporte do Elasticsearch 7 foi anunciado para agosto de 2023 e o 2.4.8 já está otimizado para OpenSearch 3. A decisão é mais de licenciamento/suporte do que técnica.

Configurar o engine pela CLI

Pare de configurar isso só pelo Admin — em CI/CD você precisa do comando:

bin/magento config:set catalog/search/engine opensearch
bin/magento config:set catalog/search/opensearch_server_hostname 127.0.0.1
bin/magento config:set catalog/search/opensearch_server_port 9200
bin/magento config:set catalog/search/opensearch_index_prefix loja_prod
bin/magento config:set catalog/search/opensearch_server_timeout 15
bin/magento indexer:reindex catalogsearch_fulltext
bin/magento cache:flush

Testar conectividade e saúde do cluster

curl -s localhost:9200/_cluster/health?pretty

Procure "status": "green". Em single-node, é normal ver yellow — porque há réplicas não alocadas (não há segundo nó para hospedá-las). A correção é definir replicas = 0 (réplica só faz sentido com mais de um nó).

Tuning de JVM heap (o que mais derruba o search)

O OpenSearch/Elasticsearch é Java; o heap define a memória disponível. Regra consolidada: -Xms e -Xmx iguais, em ~50% da RAM da máquina, nunca acima de ~31 GB. Acima de ~32 GB a JVM perde os compressed ordinary object pointers (compressed oops) e passa a usar ponteiros de 64 bits, inflando o uso de memória em ~1,5x — você paga mais RAM por menos heap útil.

# /etc/opensearch/jvm.options  (ou jvm.options.d/)
-Xms16g
-Xmx16g   # 50% de 32GB, abaixo do teto de 31g; mesmo valor que -Xms

Os outros 50% de RAM ficam para o cache de sistema operacional, que o Lucene usa intensamente. (Opster — OpenSearch heap & GC)

Índices, prefixo e reindex

O reindex do indexer catalogsearch_fulltext reconstrói os índices físicos (ex.: catalog_product_*) que o OpenSearch consulta. O index_prefix isola ambientes que compartilham o mesmo cluster (ex.: loja_prod vs loja_stg) — sem ele, staging e produção brigam pelos mesmos índices. Índices órfãos (de ambientes apagados) ficam ocupando disco; liste e limpe pelo prefixo:

curl -s 'localhost:9200/_cat/indices?v' | grep loja_
# remover órfão específico:
curl -X DELETE 'localhost:9200/loja_stg_catalog_product_v123'

Alternativa moderna: Live Search (SaaS)

O Adobe Commerce Live Search tira o catalogsearch da instância local, levando indexação e relevância para um serviço SaaS — alivia CPU/RAM do servidor e remove o cluster de busca da sua operação. Vale avaliar em catálogos grandes onde manter OpenSearch local vira custo operacional.

Componente2.4.7-p102.4.8-p5
PHP8.3 / 8.28.4 / 8.3
MariaDB10.11 / 11.811.4 / 11.8
MySQLnão suportado8.4
OpenSearch2 / 33
Elasticsearch88

Confirme sempre as versões para a sua linha na página de System Requirements — elas mudam por patch level. Busca rápida e completa também é SEO técnico: categoria que estoura timeout ranqueia pior.

Otimização do Composer e sequência de deploy sem surpresas

O autoloader do Composer resolve classes por regras PSR-4/PSR-0 consultando o filesystem. Em produção, isso é desperdício.

O comando que deve estar em todo deploy

composer dump-autoload --optimize

A flag --optimize (-o) converte PSR-4/PSR-0 em um classmap estático. A documentação do Composer é direta:

There are no real trade-offs with this method. It should always be enabled in production.

Fonte: Composer — Autoloader optimization

Flags avançadas e seus cuidados

  • --optimize (-o): seguro e recomendado sempre. Gera o classmap.
  • --classmap-authoritative (-a): assume que classe fora do classmap não existe — não consulta o filesystem. Cuidado: classes geradas em runtime quebram. No Magento, como você já pré-gera tudo via di:compile, costuma ser seguro, mas teste.
  • --apcu: cache APCu como fallback do autoloader. Seguro, mas não combina com -a.

Sequência de deploy que uso (servidor próprio)

  1. bin/magento maintenance:enable
  2. composer install --no-dev
  3. composer dump-autoload --optimize
  4. bin/magento setup:upgrade --keep-generated
  5. bin/magento setup:di:compile
  6. bin/magento setup:static-content:deploy pt_BR en_US -j $(nproc) -s standard
  7. bin/magento cache:flush + reload php-fpm (resetar OPcache!)
  8. bin/magento maintenance:disable

O passo 7 inclui o reload do PHP-FPM porque, com opcache.validate_timestamps=0, sem isso a loja continua servindo o bytecode antigo. Em produção séria, prefiro blue-green / symlink atômico de release para manter o downtime perceptível em zero. Esse rigor de deploy também fecha janelas de exposição — detalho na segurança e hardening do Magento 2.

JS/CSS: minify, o problema do RequireJS, critical CSS, defer e fontes

Reduzir e organizar o que vai ao navegador ataca diretamente o LCP e, sobretudo, o INP. O Magento tem controles nativos em Stores > Configuration > Advanced > Developer (a aba só aparece em modo developer; em produção use config:set).

Configuração nativa — com ressalva de HTTP/2

OpçãoPath de configMinha recomendação
Minify JavaScriptdev/js/minify_filesYes — sempre vale
Minify CSSdev/css/minify_filesYes — sempre vale
Minify HTMLdev/template/minify_htmlYes
Merge JavaScriptdev/js/merge_filesCautela — medir antes/depois
Merge CSSdev/css/merge_css_filesCautela — medir antes/depois

Trade-off que muitos guias ignoram: com HTTP/2 (multiplexação), juntar tudo num arquivo gigante perde valor e pode piorar, porque quebra o caching granular (mudou 1 byte, o cliente rebaixa o bundle inteiro). Minify: sim, sempre. Merge/bundle nativo: com cautela e medindo — em muitas lojas em HTTP/2 eu deixo merge desligado.

A real causa de INP ruim no Magento: RequireJS/AMD

O frontend Luma carrega JS via RequireJS (AMD). Isso significa muitos módulos resolvidos e executados no main thread, gerando long tasks que travam a resposta a cliques — exatamente o que o INP penaliza. Minify e merge não resolvem isso; o que ajuda é carregar menos JS crítico e adiar o resto.

Critical CSS + carregar o resto sem bloquear

Extraia o CSS do above-the-fold, faça inline no <head> e carregue o restante de forma não-bloqueante:

<style>/* critical CSS do above-the-fold, inline */</style>
<link rel="preload" href="/static/.../styles-m.css" as="style"
      onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/static/.../styles-m.css"></noscript>

Defer/async para scripts não-críticos

Scripts que não participam da primeira pintura devem ser defer/async (analytics, chat, widgets). Em Magento, isso passa por mover dependências para fora do bundle crítico e revisar o que o tema injeta no <head>. Cada script tirado do caminho crítico reduz long tasks e melhora o INP.

Fontes: evite FOIT e proteja o LCP

@font-face {
  font-family: 'Loja';
  src: url('/fonts/loja.woff2') format('woff2');
  font-display: swap;   /* ou 'optional' p/ não bloquear */
}
<link rel="preload" href="/fonts/loja.woff2"
      as="font" type="font/woff2" crossorigin>

font-display: swap evita o texto invisível (FOIT) e o preload da fonte do above-the-fold tira o atraso do LCP quando o título é texto.

Baler: cuidado em 2026

O Baler (magento/m2-baler) é um bundler inteligente, mas seja honesto sobre o estado dele: o repositório está praticamente sem manutenção, sem releases publicados e ainda marcado como alpha. Adotá-lo hoje é um risco. Antes de apostar nele, avalie um build customizado (esbuild/webpack via parceiro) ou — o caminho estrutural — trocar o frontend, como discuto na seção de Hyvä. Se ainda assim usar o Baler, desligue minify/merge/bundle nativos (ele não convive com eles):

bin/magento config:set dev/js/minify_files 0
bin/magento config:set dev/js/enable_js_bundling 0
bin/magento config:set dev/js/merge_files 0

Frontend moderno: Hyvä e PWA Studio como caminho estrutural para INP

Um especialista em Core Web Vitals em 2026 não pode ignorar o elefante na sala: o tema Luma com RequireJS é o maior lastro de JS e de INP do Magento. Você pode minificar, bundlar e adiar o quanto quiser — enquanto o frontend carregar dezenas de módulos AMD e jQuery no main thread, o teto de INP continua baixo. A solução estrutural é trocar a camada de apresentação.

Hyvä: o caminho mais pragmático hoje

O Hyvä reescreve o frontend do Magento sem RequireJS e sem jQuery, usando Tailwind CSS e Alpine.js (poucos KB). O resultado típico é uma queda drástica no JavaScript do main thread e Lighthouse de mobile que, no Luma, eram inalcançáveis. Por que isso ataca o INP na raiz:

  • Sem RequireJS: some a cascata de módulos AMD que gerava long tasks.
  • Alpine.js no lugar de jQuery/Knockout: ordens de magnitude menos JS para parsear e executar.
  • HTML server-rendered + ilhas de interatividade leves: menos trabalho no cliente, melhor INP e LCP.

O trade-off honesto: é uma reescrita de tema (licença + esforço de migração de templates e de compatibilidade de módulos de terceiros). Não é "ligar uma config"; é decisão de projeto. Mas, quando o objetivo é INP < 200 ms consistente em mobile, é o caminho que mais entrega.

PWA Studio (headless)

O PWA Studio (React/GraphQL) desacopla o frontend via API. Entrega excelente experiência de SPA, mas é um projeto bem mais pesado: build, SSR para SEO, e uma stack JS própria para manter. Recomendo para times com músculo de frontend e necessidade real de experiência app-like; para a maioria das lojas que só quer aprovar nos CWV, Hyvä tem melhor relação custo/benefício.

Como decidir

CaminhoEsforçoGanho de INP/JSQuando recomendo
Otimizar Luma (critical CSS, defer)BaixoIncremental (teto limitado)Loja pequena, orçamento curto
HyväMédioAltoQuem precisa aprovar CWV de verdade
PWA StudioAltoAltoTime com frontend forte, experiência app-like

Imagens e LCP: como o Magento serve WebP, srcset, catalog:images:resize e o lazy automático

Na maioria das lojas, o elemento de LCP é uma imagem — o banner do topo ou a foto principal do produto. Otimizar imagem é otimizar LCP. Mas há armadilhas específicas do Magento que os snippets genéricos escondem.

O gotcha número um: o Magento NÃO gera WebP/AVIF nativamente

Você coloca banner.webp no snippet — mas de onde vem o .webp? O Magento não converte para WebP/AVIF sozinho. As opções reais são:

  • (a) Fastly IO na borda: detecta o navegador e serve WebP/AVIF on-the-fly (caminho do Adobe Commerce Cloud).
  • (b) CDN com otimização de imagem: Cloudflare Images, imgix etc.
  • (c) Módulo de terceiros / pipeline de build: gera as variações no deploy.

Sem uma dessas, o src="...webp" do exemplo simplesmente não existe no disco. Saber disso evita horas perdidas.

web.dev sobre formatos modernos

Segundo a documentação do web.dev sobre LCP, usar formatos como WebP e AVIF é uma forma recomendada de reduzir o tamanho dos recursos, porque comprimem melhor que PNG/JPEG e resultam em downloads mais rápidos (a página não crava um percentual). (web.dev — Optimize LCP)

O snippet certo: responsivo (srcset/sizes), não só width/height

Servir uma imagem de 1280px para um celular é desperdício de LCP mobile. Use srcset/sizes para o navegador escolher o tamanho por viewport:

<img src="/media/banner-1280.webp"
     srcset="/media/banner-640.webp 640w,
             /media/banner-1280.webp 1280w"
     sizes="(max-width:768px) 100vw, 1280px"
     width="1280" height="480"
     fetchpriority="high"
     loading="eager"
     alt="Promoção da semana">

width/height reservam espaço e matam o CLS; fetchpriority="high" prioriza o download; loading="eager" garante que essa imagem não seja adiada.

O gotcha do lazy automático do Magento 2.4.x

O Magento 2.4.x aplica loading="lazy" automaticamente em imagens — ótimo abaixo da dobra, mas desastroso se pegar a imagem LCP (banner/primeiro slide/foto principal do PDP). O navegador só busca a imagem lazy depois de calcular o layout, atrasando o LCP. Garanta loading="eager" + fetchpriority="high" no primeiro elemento de imagem da dobra. Para imagens below-the-fold, deixe o lazy nativo agir.

Cache de imagem e regeneração

O Magento mantém variações redimensionadas em pub/media/catalog/product/cache/. Para (re)gerar todas as variações de catálogo (útil no deploy ou após mudar a configuração de imagem):

bin/magento catalog:images:resize

Limpar esse diretório força a regeração na próxima requisição (pesado se feito em pico — prefira rodar o images:resize em janela controlada).

Qualidade de imagem na origem

A qualidade de compressão das imagens geradas pelo Magento é configurável (área de configuração de upload/imagem do produto). Baixar a qualidade JPEG/WebP de 90 para ~75-80 costuma cortar bytes sem perda visual perceptível — ganho direto de LCP. É a ponte entre "use WebP" e "como o Magento produz isso": o pipeline gera a variação, a qualidade define o peso, e a entrega WebP fica por conta de Fastly IO/CDN.

CDN/Fastly, shielding, e a configuração real de Nginx (Brotli, HTTP/2, far-future cache)

Uma CDN aproxima os assets do visitante e tira carga da origem. Vou separar o que é performance (edge cache, shielding, IO, compressão) do que é segurança (WAF/DDoS) — misturar os dois dilui a discussão.

Fastly: o que é performance

No Adobe Commerce on cloud infrastructure, o Fastly (CDN baseado em Varnish) vem incluído e faz cache de páginas, assets e CSS na borda. Para performance, o que importa:

  • Edge caching: serve a resposta da borda, mais perto do usuário.
  • Fastly Image Optimization (IO): resize e conversão para WebP/AVIF on-the-fly — resolve a entrega de imagem moderna que o Magento não faz sozinho.
  • Shielding (origin shield): ver abaixo.

(WAF gerenciado e proteção DDoS também vêm no Fastly, mas são segurança — trato no hardening, não aqui.) (Adobe — Fastly CDN)

Shielding: o detalhe que multiplica o hit ratio

Sem shielding, cada POP da CDN que tem MISS vai direto à sua origem — em uma CDN global, isso pode significar dezenas de MISS para o mesmo conteúdo. O origin shield designa um POP intermediário por onde passam todos os MISS: os demais POPs buscam dele, não da origem. Resultado: a origem recebe muito menos requisições, o hit ratio global sobe e você protege o backend de picos. Ativa-se no painel do Fastly escolhendo o POP de shield mais próximo da origem.

Nginx: Brotli de verdade (não só gzip)

Brotli comprime texto melhor que gzip. O gotcha: ngx_brotli precisa ser compilado como módulo dinâmico — não vem no Nginx padrão. Depois de instalá-lo:

brotli on;
brotli_comp_level 5;
brotli_types text/css application/javascript application/json image/svg+xml;

# fallback gzip para clientes sem Brotli
gzip on;
gzip_comp_level 5;
gzip_types text/css application/javascript application/json image/svg+xml;

HTTP/2 (sintaxe nova) e HTTP/3

No Nginx 1.25+, a diretiva mudou — não é mais listen 443 ssl http2;:

server {
    listen 443 ssl;
    http2 on;            # sintaxe nova (Nginx 1.25+)
    # http3 on; quic_retry on;   # HTTP/3 / QUIC: próximo passo
}

Far-future caching de assets estáticos

Assets versionados pelo Magento (static/version{N}/) podem ter cache agressivo na origem, porque a URL muda quando o conteúdo muda:

location ~* \.(js|css|woff2|webp|avif)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Isso funciona junto com o versionamento de static content do Magento: o número de versão na URL invalida o cache quando você deploya, então o immutable é seguro. É performance pura de origem, de graça.

Core Web Vitals: LCP, INP, CLS e como cada um se conecta ao Magento

Os Core Web Vitals quantificam carregamento, responsividade e estabilidade visual. Os limiares "good" são medidos no percentil 75 dos carregamentos, em mobile e desktop.

MétricaO que medeAlvo "good" (p75)
LCPCarregamento do maior elemento visívelaté 2,5 s
INPResponsividade às interações200 ms ou menos
CLSEstabilidade visual (saltos de layout)0,1 ou menos

Segundo a documentação do web.dev, o LCP deve ocorrer em até 2,5 s; o INP deve ser ≤ 200 ms; e o CLS ≤ 0,1 — sempre no 75º percentil, segmentado entre mobile e desktop. (web.dev — Web Vitals)

INP substituiu o FID

Em 12 de março de 2024 o INP tornou-se Core Web Vital e substituiu o FID. O FID saiu do Search Console nessa data; outras ferramentas tiveram seis meses de descontinuação. O INP havia sido introduzido como experimental pela equipe do Chrome em maio de 2022. (web.dev — INP becomes a CWV)

Como cada CWV se resolve no Magento

  • LCP: TTFB rápido (Varnish/Fastly + OPcache), imagem LCP em WebP com srcset, fetchpriority="high" e sem lazy; fonte do título com preload.
  • INP: menos JS no main thread — critical CSS, defer, e estruturalmente o frontend (Hyvä) contra o RequireJS do Luma.
  • CLS: width/height em imagens, espaço reservado para banners e evitar inserções tardias.

O que o Google usa para ranqueamento são os field data (usuários reais), não o número de lab do Lighthouse — daí o fluxo de diagnóstico da próxima seção. Para cruzar com os demais sinais de busca, veja o SEO técnico para Magento 2.

Banco: my.cnf real, slow query log, tabelas que incham, indexers e message queue

O banco é o coração transacional do Magento. Vou separar isto em subtópicos para cada um ter substância, em vez de um amontoado raso.

Indexers: Update on Save vs Update by Schedule (Mview)

  • Update on Save: reindexa a cada save — com vários admins salvando, gera deadlocks e alto CPU.
  • Update by Schedule (Mview): reindexa via cron, parcial e incremental. É o recomendado em produção.
bin/magento indexer:set-mode schedule
bin/magento indexer:show-mode
bin/magento indexer:reindex   # completo, só após import em massa

Segundo a Adobe, para evitar deadlocks quando vários usuários do Admin disparam reindexação, todos os indexers devem rodar agendados (Update by Schedule). (Adobe — Index management)

my.cnf de produção (MySQL/MariaDB)

[mysqld]
# buffer pool: 50-70% da RAM dedicada ao banco
innodb_buffer_pool_size = 12G
innodb_buffer_pool_instances = 8        # ~1 instância por 1-2GB de pool
innodb_log_file_size = 2G               # writes pesados (reindex/import)
innodb_flush_log_at_trx_commit = 2      # throughput x durabilidade (ver nota)
innodb_flush_method = O_DIRECT

tmp_table_size = 128M
max_heap_table_size = 128M              # par com tmp_table_size

max_connections = 300
thread_cache_size = 64

Nota sobre innodb_flush_log_at_trx_commit=2: ele grava o log a cada commit mas só faz flush ao disco 1x/segundo — ganho de throughput grande, ao custo de poder perder ~1s de transações num crash do SO. Em e-commerce com replicação, costumo aceitar esse trade-off; em ambiente com requisito de durabilidade estrita, mantenha 1.

Slow query log: ative e leia

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;            -- segundos
SET GLOBAL log_queries_not_using_indexes = 1;
-- arquivo: SHOW VARIABLES LIKE 'slow_query_log_file';

Use mysqldumpslow ou pt-query-digest para agrupar as queries mais caras. É o ponto de partida para achar a query N+1 que o profiler vai confirmar.

Tabelas que incham (e travam backup e buffer pool)

Estas crescem indefinidamente em loja com tráfego e precisam de limpeza:

  • report_event, report_viewed_product_index, report_compared_product_index — relatórios/recently viewed.
  • customer_visitor, customer_log, e diversas *_log.
  • quote / quote_item antigos — carrinhos abandonados acumulados.

Elas estufam o backup e ocupam buffer pool com dado morto. Há o mecanismo de log cleaning (Stores > Configuration > Advanced > System > Log Cleaning, via cron) e, para quotes velhos, truncamento seguro com filtro de data. Antes de qualquer DELETE/TRUNCATE em produção: backup e janela.

Message queue: MySQL DB queue vs RabbitMQ

Por padrão o Magento usa o MySQL como sistema de filas (tabelas queue, queue_message, queue_message_status), processado pelo cron. Funciona, mas com alto volume de mensagens assíncronas (async indexing, e-mails, exports), o banco vira gargalo. Aí migra-se para RabbitMQ/AMQP, desenhado para isso. Os consumers processam as filas:

bin/magento queue:consumers:list
bin/magento queue:consumers:start async.operations.all --max-messages=1000

Em produção, os consumers rodam pelo cron via configuração em env.php (detalho na seção de consumers/cron). (Adobe — Manage message queues)

Flat catalog: pode esquecer

O flat catalog não é mais best practice e é conhecido por causar degradação e problemas de indexação. Se sua loja ainda tem por herança, desligar costuma ser ganho, não perda.

Catálogos grandes com integração de estoque/preço via ERP e NF-e geram carga de escrita que estressa os indexers — dimensione o banco pensando nesse fluxo também.

Consumers, cron groups e processamento assíncrono

Performance percebida não é só TTFB: se o cron está parado ou mal configurado, indexers não atualizam, e-mails não saem e operações assíncronas se acumulam — a loja "parece" lenta e desatualizada. Esta camada é frequentemente o gargalo invisível.

Garanta que o cron está rodando

bin/magento cron:install      # instala a crontab do Magento
# monitore jobs presos:
# SELECT job_code,status,COUNT(*) FROM cron_schedule GROUP BY 1,2;

Uma das causas mais comuns de "indexer invalidado que nunca atualiza" é simplesmente o cron parado. Outra é um único processo cron sequencial tentando rodar tudo e virando gargalo.

Cron groups

O Magento separa jobs em grupos (index, default, consumers etc.). Rodá-los com paralelismo adequado evita que o reindex segure o envio de e-mails, por exemplo. Monitore a cron_schedule por jobs em status running presos — eles bloqueiam o grupo.

Consumers via cron_consumers_runner (env.php)

Em produção, não rode consumers à mão indefinidamente; configure o runner no app/etc/env.php:

'cron_consumers_runner' => [
    'cron_run' => true,           // habilita o runner pelo cron
    'max_messages' => 1000,       // msgs por execução antes de terminar
    'consumers' => [],            // [] = todos; ou liste os que quer
    'multiple_processes' => [],   // ex.: rodar N processos de um consumer
],

Segundo a documentação da Adobe, cron_run (default true) liga/desliga o cron job dos consumers; max_messages (default 10000) é o teto de mensagens por consumer antes de terminar; e consumers vazio roda todos. (Adobe — Manage message queues)

Async indexing e async email

Recursos como async indexing e async email tiram trabalho pesado do request do usuário e o jogam para a fila — melhoram a performance percebida (a página responde já; o trabalho acontece em background). Mas eles dependem de consumers saudáveis: ligar async sem garantir que os consumers rodam só esconde o atraso na fila. Monitore o backlog (tamanho das tabelas de fila ou filas do RabbitMQ) como parte do health check.

Auditoria de extensões: medindo o overhead de módulos de terceiros

Na minha experiência de campo, a causa real número um de loja lenta não é a stack mal configurada — é um punhado de módulos de terceiros caros: um plugin around em load(), uma collection sem filtro, um observer que faz query externa em cada page view. Nenhuma quantidade de Varnish salva uma página dinâmica que o módulo torna não-cacheável. Por isso, auditar extensões é parte do trabalho de performance.

Inventário: o que está ligado

bin/magento module:status                 # habilitados vs desabilitados
bin/magento module:disable Vendor_Modulo  # desligar o que não se usa
bin/magento setup:upgrade && setup:di:compile

Loja antiga acumula módulos de testes e POCs que ninguém desligou. Cada um adiciona plugins, observers, layout XML e, às vezes, JS no frontend. Desligar o que não se usa é ganho líquido.

O profiler nativo do Magento (MAGE_PROFILER)

Antes de pagar por APM, o profiler embutido já mostra onde o tempo vai por bloco/observer/plugin:

# habilita o profiler em HTML para uma análise pontual
export MAGE_PROFILER=html
# (ou via app/etc/config.php; saída ao final da página)

Ele revela, por exemplo, um bloco que sozinho consome 300 ms — geralmente um módulo de terceiros renderizando algo pesado em toda página. Desligue o profiler depois; ele tem overhead.

Sinais de extensão problemática

  • Página de catálogo com Set-Cookie: módulo tornando a resposta privada e furando o Varnish.
  • Plugin around em load()/getProductCollection(): candidato a N+1; o Blackfire confirma.
  • JS pesado injetado no <head>: piora INP; veja o waterfall.
  • Queries externas (API) em observer síncrono: some o TTFB com a latência do terceiro.

Minha regra: a cada onboarding de loja lenta, antes de tunar a stack, rodo module:status + profiler em uma PDP e uma PLP. Em metade dos casos, o vilão aparece aí.

Medição e diagnóstico: o fluxo campo → lab → APM → profiler

Não dá para otimizar o que não se mede — mas medir bem é ter um fluxo, não uma lista de ferramentas. O erro comum é confundir dado de laboratório com dado de campo: eles respondem perguntas diferentes.

Lab vs field

FerramentaTipoO que só ela entrega
LighthouseLabAuditoria reproduzível antes/depois num device fixo
PageSpeed InsightsLab + FieldCombina CrUX (real) com Lighthouse (simulado)
WebPageTestLabWaterfall com TTFB e Start Render, filmstrip, múltiplas conexões/localidades
Search Console (CWV)FieldAgrupa URLs por status good/needs improvement/poor (base CrUX)

Segundo a documentação do PSI, a ferramenta mostra lab data (gerado pelo Lighthouse, simulando o load) e field data de usuários reais vindo do Chrome User Experience Report (CrUX). É por isso que os números divergem: um mede a realidade, o outro a simulação. (Google — About PSI)

O fluxo de diagnóstico que uso

  1. Campo aponta o problema: Search Console (relatório de Core Web Vitals) ou CrUX mostram QUAL métrica está ruim e em QUAL grupo de URLs (PDP, PLP, home). Comece sempre pelo campo — é o que ranqueia.
  2. Reproduzir em lab e ler o waterfall: rode Lighthouse/WebPageTest na URL afetada. No waterfall, separe TTFB (problema de servidor: cache/Varnish/PHP/DB) do tempo de render (problema de cliente: CSS/JS/imagem). É essa leitura que decide o próximo passo.
  3. Se for servidor → APM: o transaction trace do New Relic mostra onde o tempo vai dentro do PHP — método caro, SQL N+1, chamada externa lenta.
  4. Confirmar a causa raiz → Blackfire: profile pontual no endpoint para achar o método/query específico que domina o tempo.

WebPageTest: o diferencial para LCP

O WebPageTest entrega filmstrip, waterfall com TTFB e Start Render e teste em múltiplas conexões/localidades. Para isolar LCP, é o que separa "servidor demorou a responder" de "a imagem demorou a pintar" — diagnóstico que o PSI sozinho não dá.

CrUX para tendência e RUM próprio para INP

  • CrUX Dashboard (Looker/BigQuery): tendência histórica de campo mês a mês — útil para provar regressão ou melhoria ao longo do tempo.
  • RUM próprio com a lib web-vitals (JS): coleta INP por interação real. É importante porque INP de lab é pouco confiável (o lab não clica como o usuário); medir em produção mostra QUAL interação trava.

APM e profiling: notas de campo

  • New Relic: incluído nos planos Adobe Commerce Cloud Pro (chave já provisionada). APM no server-side (PHP, SQL, APIs) e Browser/RUM no client-side, incluindo CWV.
  • Blackfire.io: profiler PHP que rastreia cada chamada, método e query. Suba um profile com a CLI/extensão de browser, leia o call graph e ache o hot path (collection N+1, plugin caro, autoload). Rode sob demanda, nunca contínuo em produção. O acesso gratuito ao Blackfire deixou de vir incluído no Adobe Commerce on cloud desde 11 de abril de 2020 — hoje exige licença direta com a Blackfire.io.

Performance budget e monitoramento contínuo: para não regredir

Otimizar uma vez e seguir a vida é receita para regressão: cada deploy, cada módulo novo, cada banner do marketing pode estragar o que você ganhou. Especialista de verdade trata performance como contrato com CI, não como tarefa pontual.

Defina um performance budget

Estabeleça tetos explícitos e versionados — por exemplo: LCP < 2,5 s, INP < 200 ms, CLS < 0,1, peso de JS da home < X KB, peso total da PDP < Y KB. O budget transforma "a loja está lenta" (subjetivo) em "o deploy estourou o budget de JS em 40 KB" (acionável).

Lighthouse CI no pipeline

Rode Lighthouse a cada PR/deploy e falhe o build quando o budget for ultrapassado:

# exemplo de assert de budget no Lighthouse CI
{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-byte-weight": ["warn", { "maxNumericValue": 1600000 }]
      }
    }
  }
}

Assim, a regressão é barrada antes de chegar à produção — muito mais barato que descobrir pelo Search Console semanas depois.

Alertas de regressão de CWV em campo

Complemente o lab com vigilância de campo: alerta quando o relatório de Core Web Vitals do Search Console muda um grupo de URLs de "good" para "needs improvement", ou quando o RUM (lib web-vitals) acusa o p75 de INP subindo. A combinação budget no CI (lab) + alerta no campo (CrUX/RUM) é o que mantém o ganho ao longo do tempo, em vez de um pico de performance que decai a cada sprint.

Perguntas frequentes

Qual é a configuração que mais acelera uma loja Magento 2 lenta?

Production mode + Full Page Cache adequado é o maior ganho imediato. bin/magento deploy:mode:set production compila o DI e gera o static content; ativar o Varnish faz as páginas serem servidas direto da memória em vez de reprocessadas pelo PHP. Mas, na minha experiência, a causa nº 1 de loja lenta costuma ser um punhado de módulos de terceiros caros — por isso rodo module:status e o profiler nativo (MAGE_PROFILER) numa PDP/PLP antes de tunar a stack.

Como medir o hit ratio do Varnish de verdade, e não só de uma página?

Inspecionar HIT/MISS via curl numa URL é só diagnóstico pontual. Para o agregado, use varnishstat -1 -f MAIN.cache_hit,MAIN.cache_miss e calcule cache_hit/(cache_hit+cache_miss). Para achar o que fura o cache, varnishtop -i ReqURL e varnishhist. A causa mais comum de hit ratio baixo é Set-Cookie em página pública (módulo mal-comportado), que torna a resposta privada e a tira do cache.

Como o Magento mantém a página cacheada no Varnish mostrando mini-cart e dados do cliente?

A página inteira é pública e cacheável; o conteúdo privado (carrinho, wishlist, 'Olá, Fulano') é injetado no navegador pela lib customer-data via Ajax em /customer/section/load, controlado pelo cookie private_content_version. Como o dado privado vive fora do HTML cacheado, o Varnish pode servir a casca da RAM. Alternativamente, blocos ESI (esi:include) montam fragmentos com TTL próprio na borda. É por isso que qualquer Set-Cookie em página de catálogo fura o cache.

Qual maxmemory-policy usar no Redis do Magento?

Depende do papel da instância. Para default cache e page_cache use allkeys-lru (cache é descartável). Para a instância de SESSÃO use noeviction ou volatile-lru — nunca allkeys-lru, porque sob pressão de memória o Redis expulsaria sessões ativas e deslogaria clientes no meio do checkout. Por isso eu prefiro instâncias Redis separadas para cache e sessão em lojas de porte, cada uma com sua política.

Por que minha alteração de código 'não pega' depois do deploy?

Quase sempre é OPcache. Com opcache.validate_timestamps=0 em produção (recomendado para performance), o PHP nunca checa se o arquivo mudou no disco e continua servindo o bytecode antigo. Todo deploy precisa resetar o OPcache, normalmente com sudo systemctl reload php8.3-fpm. Se você usa opcache.preload, é preciso restart (não reload) do FPM. Outra causa é generated/ velho — rode setup:di:compile.

O Magento gera WebP automaticamente para melhorar o LCP?

Não. O Magento não converte imagens para WebP/AVIF nativamente. Para servir WebP você precisa de (a) Fastly Image Optimization na borda (Adobe Commerce Cloud), (b) uma CDN com otimização de imagem, ou (c) um módulo/pipeline de build que gere as variações. Sem isso, o src='.webp' do snippet não existe. Some a isso srcset/sizes para o tamanho certo por viewport, width/height contra CLS, e fetchpriority=high + loading=eager na imagem LCP — cuidando do lazy automático do 2.4.x, que pode pegar o banner.

Como diagnosticar se um LCP ruim é problema de servidor ou de navegador?

Siga o fluxo: o campo (Search Console/CrUX) aponta a métrica e o grupo de URLs ruim; reproduza em WebPageTest e leia o waterfall. Se o TTFB é alto, o problema é servidor (cache/Varnish/PHP/DB) — investigue com New Relic APM e confirme o método/query culpado no Blackfire. Se o TTFB é baixo mas o Start Render/LCP demora, o problema é cliente (CSS/JS/imagem). O WebPageTest é insubstituível aqui por dar filmstrip e a separação TTFB vs Start Render.

Ainda devo usar Redis ou já preciso migrar para o Valkey?

Depende da linha. A página oficial de System Requirements já lista o Redis como não suportado no 2.4.7-p10, indicando o Valkey 8.1; a partir do 2.4.8 o Valkey 8 entra como alternativa compatível (mesmos flags de CLI, mesmo env.php) e no 2.4.9 substitui o Redis na ferramenta de linha de comando. Os comandos Redis seguem válidos no 2.4.8, mas em stack nova ou upgrade considere o Valkey como caminho de longo prazo. Confirme sempre na página oficial.

Vale a pena migrar o tema para Hyvä por causa dos Core Web Vitals?

Se o objetivo é INP < 200 ms consistente em mobile, frequentemente sim. O Luma carrega JS via RequireJS/AMD + jQuery, gerando long tasks no main thread que limitam o INP por mais que você minifique e adie. O Hyvä reescreve o frontend com Tailwind + Alpine.js, cortando drasticamente o JS. O trade-off é o esforço de reescrita de tema e compatibilidade de módulos. Para loja pequena, otimizar o Luma (critical CSS, defer, fontes) já ajuda; para aprovar CWV de verdade, Hyvä costuma ter a melhor relação custo/benefício.

Referências oficiais

  1. Set the operation mode (deploy:mode:set) — Adobe Experience League
  2. Deploy static view files (setup:static-content:deploy, --jobs) — Adobe Experience League
  3. Caching overview (Varnish strongly recommended for production) — Adobe Experience League
  4. Configure Varnish for Commerce (varnish:vcl:generate) — Adobe Experience League
  5. Configure Varnish ESI blocks — Adobe Experience League
  6. Private content (customer-data, section/load, private_content_version) — Adobe Developer
  7. Use Redis for the page cache (env.php structure) — Adobe Experience League
  8. Cm_RedisSession (session locking, max_concurrency, compression) — Colin Mollenhour (GitHub)
  9. Search engine overview (Elasticsearch / OpenSearch) — Adobe Experience League
  10. OpenSearch heap size usage and JVM garbage collection (31GB / compressed oops) — Opster
  11. Manage message queues (RabbitMQ, cron_consumers_runner, consumers) — Adobe Experience League
  12. System requirements (Adobe Commerce 2.4.7 / 2.4.8) — Adobe Experience League
  13. Autoloader optimization (--optimize / -o, -a, --apcu) — Composer (getcomposer.org)
  14. Web Vitals (LCP, INP, CLS thresholds) — web.dev (Google)
  15. Optimize Largest Contentful Paint — web.dev (Google)
  16. INP becomes a Core Web Vital on March 12 — web.dev (Google)
  17. About PageSpeed Insights (field vs lab data) — Google for Developers
  18. Index management (Update by Schedule / Mview) — Adobe Experience League
  19. Fastly services overview (CDN, IO, shielding for Adobe Commerce on cloud) — Adobe Experience League
  20. Lighthouse overview — Chrome for Developers
Precisa de um orçamento? Ficarei feliz em ajudar. Clique Aqui