← Voltar ao blog

Páginas de categoria lentas no Magento 2: checklist de profiling

Rodrigo Biazzi

As páginas de categoria são onde os problemas de performance do Magento 2 costumam aparecer primeiro. Elas combinam layered navigation, uma collection de produtos com preços para o grupo do cliente atual, imagens, swatches, reviews e mais o que houver de blocos de terceiros na listagem. Quando uma categoria leva vários segundos para renderizar, a causa raramente é uma coisa só, e chutar desperdiça dias.

Este é o checklist que eu sigo no Magento Open Source e no Adobe Commerce 2.4.x. Ele começa pela medição, porque todo passo seguinte depende de saber onde o tempo vai.

1. Confirme o básico antes de fazer profiling

Alguns problemas são de configuração, não de código. Confira estes primeiro, levam minutos:

  • Modo production. bin/magento deploy:mode:show deve dizer production. O modo developer compila código e arquivos estáticos na hora e é muito mais lento.
  • Todos os caches ligados. bin/magento cache:status. Um cache layout, block_html ou full_page desativado muda tudo.
  • Indexers em schedule e atualizados. bin/magento indexer:status. Indexers marcados como invalid ou com um backlog grande significam que a loja lê tabelas de índice velhas ou incompletas.
  • Cron rodando. Com os indexers em Update by Schedule, nada é reindexado se o cron não roda. Veja na cron_schedule se há jobs presos em running ou se acumulando como pending.
  • Backends de cache e sessão. Redis ou Valkey para cache e sessões, não o sistema de arquivos nem o banco.
  • Saúde do mecanismo de busca. Layered navigation e listagem de categoria passam pelo OpenSearch. Um cluster em estado yellow ou red, ou faminto de heap, aparece como páginas de categoria lentas.
bin/magento deploy:mode:show
bin/magento cache:status
bin/magento indexer:status
bin/magento indexer:show-mode
curl -s "http://localhost:9200/_cluster/health?pretty"

2. Separe a performance com e sem cache

Uma categoria servida do full page cache deve voltar em dezenas de milissegundos. Se não volta, o problema é o cache, não o PHP. Teste os dois casos separadamente:

curl -s -o /dev/null -w "%{time_starttransfer}\n" -I https://example.com/women/tops.html
curl -sI https://example.com/women/tops.html | grep -i -E "x-magento-cache|age|cache-control"

Se as respostas em cache são rápidas mas os usuários reais ainda veem páginas lentas, você tem um problema de hit rate. Query strings de marketing, um bloco cacheable="false" num layout que vale para categorias, ou um cookie que varia o cache podem jogar a maior parte do tráfego para fora do cache. Trato disso em detalhe no artigo sobre Varnish e full page cache.

O resto deste checklist é sobre a renderização sem cache, que todo visitante pega depois de um flush de cache, em toda URL filtrada que ainda não foi cacheada, e em toda variação de ordenação e página.

3. Faça o profiling de uma requisição sem cache

Não otimize por intuição. Pegue um trace de uma única requisição de categoria sem cache.

Profiler nativo

O Magento vem com um profiler básico. Basta para ver quais blocks e etapas de layout consomem o tempo:

bin/magento dev:profiler:enable html
# load the category page, the timings appear at the bottom
bin/magento dev:profiler:disable

Rode numa cópia de staging, não em produção, porque a saída fica visível para qualquer um que carregue a página.

Um profiler de verdade

Blackfire, Tideways, New Relic ou SPX mostram o call graph completo com queries SQL e chamadas externas. Procure três coisas:

  • Total de queries SQL. Uma página de categoria com algumas centenas de queries é comum. Milhares significa que algo carrega dados por produto.
  • Funções com alto tempo exclusivo chamadas uma vez por produto da listagem.
  • Chamadas HTTP a serviços externos durante a renderização. Isso quase nunca deveria acontecer numa página de categoria.

4. Procure cargas por produto na listagem

Uma causa muito comum de categorias lentas em código customizado é um padrão N+1: algo no template da lista de produtos ou num plugin carrega um produto completo, um stock item ou uma entidade customizada para cada produto da collection.

<?php foreach ($block->getLoadedProductCollection() as $product): ?>
    <?php $full = $productRepository->getById($product->getId()); ?>
    <?= $escaper->escapeHtml($full->getData('badge_label')) ?>
<?php endforeach; ?>

Com 36 produtos por página, são 36 carregamentos completos de produto, cada um com o seu conjunto de queries para atributos EAV, media gallery e mais. A correção é adicionar o atributo à collection uma vez, antes de ela carregar. Marcar o atributo como Used in Product Listing no admin o adiciona à collection da listagem. No código, funciona um plugin na collection:

<?php
declare(strict_types=1);

namespace Acme\Catalog\Plugin;

use Magento\Catalog\Model\ResourceModel\Product\Collection;

class AddBadgeToListing
{
    public function beforeLoad(Collection $subject, $printQuery = false, $logQuery = false): array
    {
        if (!$subject->isLoaded()) {
            $subject->addAttributeToSelect('badge_label');
        }

        return [$printQuery, $logQuery];
    }
}

Limite o escopo de um plugin assim com cuidado. Collections de produto são usadas em todo lugar, então anexá-lo globalmente adiciona o atributo a toda collection da loja. Um argumento de layout ou um plugin no block específico da lista costuma ser o alvo melhor.

Confira também verificações de estoque, resumos de review, status de wishlist ou lógica de preço customizada calculados por produto. Cada um deveria carregar os dados da página inteira numa única query e manter em memória.

5. Layered navigation e filtros

A layered navigation monta as contagens a partir de aggregations do OpenSearch. A lentidão aqui costuma vir de:

  • Atributos filtráveis demais. Cada atributo configurado como filtrável adiciona uma aggregation. Revise quais atributos realmente precisam aparecer na navegação das páginas de categoria.
  • Atributos com milhares de opções. Cada opção é um bucket na aggregation e um item no HTML renderizado.
  • Módulos de layered navigation de terceiros que adicionam as próprias queries por cima das do core, ou que desativam o full page cache para URLs filtradas.

Compare o tempo de renderização da mesma categoria com e sem layered navigation, definindo temporariamente a opção Anchor da categoria como No no staging. Se a diferença for grande, concentre-se aqui.

6. Quantidade de produtos, ordenação e paginação

  • Produtos por página. Permitir All nas páginas de categoria transforma uma listagem de 36 produtos numa de 3000. Remova dos valores permitidos em Catalog, Storefront.
  • Opções de ordenação. Só atributos marcados como Used for Sorting in Product Listing devem ser oferecidos para ordenação. Cada opção de ordenação extra é mais um conjunto de URLs que erra o cache e precisa da própria renderização sem cache.
  • Paginação profunda. A página 200 de uma categoria grande é cara para o mecanismo de busca e raramente tem valor para usuários ou SEO. Considere limitar o que é rastreado.

7. Produtos configuráveis, preços e swatches

Produtos configuráveis pesam mais nas listagens que os simples. O preço exibido é calculado a partir dos filhos, e os swatches na listagem precisam de dados dos filhos para cada opção.

  • Confira se o índice de preços está atualizado. Um índice catalog_product_price velho ou inválido leva a preços errados ou ausentes na listagem.
  • Swatches na listagem de produtos (Stores, Configuration, Catalog, Storefront, Show Swatches in Product List) carregam dados de swatch para cada produto configurável. Em configuráveis grandes isso se acumula. Desligue no staging para medir o custo e então decida.
  • Muitos grupos de clientes multiplicam o tamanho do índice de preços. Tier prices e catalog price rules aumentam mais. Isso afeta principalmente o tempo de reindex, mas um índice de preços enorme também deixa as queries da listagem mais lentas.

8. Imagens

O trabalho com imagens nunca deveria acontecer durante uma requisição de página. Se as imagens dos produtos ainda não foram redimensionadas, o Magento as gera na primeira requisição, e uma página de categoria pode disparar dezenas de resizes de uma vez.

bin/magento catalog:images:resize

Rode depois de importações, depois de mudar tamanhos de imagem no etc/view.xml do tema e depois de fazer deploy de um tema novo. Em catálogos grandes leva um tempo, então rode em segundo plano ou em lotes. Confira também se a pasta de media está em storage rápido. Páginas de categoria leem muitos arquivos pequenos, e um sistema de arquivos de rede lento aparece em toda renderização sem cache.

9. Blocks e layout

  • Procure em todo layout XML que vale para os handles catalog_category_view e default por cacheable="false". Um deles desativa o full page cache da página inteira.
  • Procure blocks que carregam a própria collection de produtos, como relacionados, vistos recentemente ou widgets promocionais colocados na sidebar da categoria. Cada um é mais um carregamento completo de collection.
  • Blocks CMS e widgets dentro da descrição da categoria rodam em toda renderização sem cache. Um widget de lista de produtos dentro da descrição de uma categoria é uma segunda listagem escondida.
  • A renderização do menu com árvores de categoria muito grandes pode ser lenta. Confira a profundidade configurada para o menu superior e se algum menu de terceiros carrega todas as categorias com todos os atributos.
grep -rn 'cacheable="false"' app/code app/design vendor/*/*/view/frontend/layout

10. Banco de dados e servidor

Quando o código da aplicação está limpo e a página continua lenta, olhe a infraestrutura:

  • Ative o slow query log do MySQL por um curto período com um limite baixo e revise o que as requisições de categoria geram.
  • Confira o tamanho do InnoDB buffer pool contra o tamanho das tabelas e índices do catálogo. Se o working set não cabe na memória, toda categoria sem cache vai ao disco.
  • Confira o número de workers do PHP-FPM e as configurações do OPcache. O OPcache precisa estar ativo com memória suficiente para guardar todo o código compilado, e o opcache.validate_timestamps pode ficar desligado em produção quando os deploys o resetam.
  • Confira se cron jobs, importações e reindexações não rodam no mesmo banco em horário de pico sem limites.

Meça de novo a cada mudança

Mude uma coisa, faça flush só do necessário, faça profiling da mesma URL de novo e anote o número. Algumas mudanças parecem boas e não fazem nada. Outras ajudam numa categoria e prejudicam outra. Uma tabela curta com a URL, a mudança e os tempos de antes e depois basta para manter o trabalho honesto e mostrar ao cliente o que mudou.

FAQ

Por que a minha página de categoria do Magento 2 é lenta só na primeira vez?

A primeira requisição renderiza a página sem full page cache e pode também disparar o redimensionamento de imagens. As requisições seguintes vêm do cache. Meça a renderização sem cache separadamente, porque URLs filtradas, ordenadas e paginadas muitas vezes também erram o cache.

Devo ativar o flat catalog para acelerar as páginas de categoria?

Não. A Adobe marcou o flat catalog de produtos como deprecated e não o recomenda no 2.4. A listagem de categoria passa pelo OpenSearch, então as tabelas flat trazem custo de indexação sem ganho claro.

Quantos produtos por página uma categoria do Magento deve mostrar?

Não há regra fixa, mas cada produto adiciona trabalho de renderização com preços, imagens e swatches. Remova a opção All, mantenha o tamanho de página padrão moderado e meça o tempo de renderização nos tamanhos que você permite.

Qual a melhor ferramenta para fazer profiling no Magento 2?

O profiler nativo mostra os tempos de block e layout e basta para uma primeira olhada. Para contagem de SQL e call graphs completos use um profiler dedicado como Blackfire, Tideways, New Relic ou SPX numa cópia de staging.

Atendo como freelancer em Magento e Adobe Commerce, de desenvolvimento de módulos e auditorias de performance a migrações e builds em Hyvä. Se isso faz sentido para você, fale comigo.

Artigos relacionados

Setup e ajuste do OpenSearch no Magento 2.4.8

Como conectar o Magento 2.4.8 ao OpenSearch, dimensionar o heap da JVM, manter o cluster green, acelerar a reindexação do catalogsearch_fulltext e depurar problemas de busca.