PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Magento 2 performance – OPcache, Redis, N+1 queries, Blackfire, priority table

by Henryk Tews / Tuesday, 14 February 2023 / Published in Magento 2

Magento 2 performance is a broad topic, but the high-impact improvements follow a clear priority order. OPcache configuration is free and immediate. Redis correctly configured reduces PHP cycles. N+1 queries are the single biggest source of slowness in custom code. Blackfire makes the invisible visible. I show a prioritised approach with concrete configurations and code examples.

Priority table – effort vs impact

Optimisation Impact Effort Where
OPcache tuning High Low (config only) php.ini
Redis for cache + session High Low env.php
Varnish FPC Very high (cached pages) Medium Infrastructure
Fix N+1 queries High (custom code) Medium PHP modules
DB indexes High Low-Medium DB schema
deploy:mode production Medium Very low CLI
JS/CSS bundle Medium (frontend) Low CLI
CDN for static files Medium Medium Infrastructure

OPcache – free performance in php.ini

[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=512
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=65407
opcache.validate_timestamps=0  ; 0 on production - no stat() calls per request
opcache.save_comments=1        ; required by Magento for annotations
opcache.consistency_checks=0

; Magento requires preloading for best performance
; opcache.preload=/var/www/html/preload.php
; opcache.preload_user=www-data
# Check OPcache status
php -r "print_r(opcache_get_status());" | grep -E "hit_rate|memory_usage"

# Hit rate should be > 99% on production
# Low memory: increase opcache.memory_consumption
# Low hit rate: increase opcache.max_accelerated_files

N+1 queries – how to find and fix

<?php

// Classic N+1 pattern - one query for the collection, N queries inside the loop
class BadOrderExport
{
    public function export(array $orderIds): array
    {
        $orders = $this->orderRepository->getList(
            $this->searchCriteriaBuilder->addFilter('entity_id', $orderIds, 'in')->create()
        )->getItems(); // 1 query

        $result = [];
        foreach ($orders as $order) {
            // getShippingAddress() triggers a separate DB query for EACH order - N queries!
            $address = $order->getShippingAddress();
            $result[] = [
                'id'      => $order->getId(),
                'total'   => $order->getGrandTotal(),
                'city'    => $address?->getCity(),
            ];
        }
        return $result; // Total: 1 + N queries
    }
}

// Fixed - load addresses in one batch query
class GoodOrderExport
{
    public function export(array $orderIds): array
    {
        $orders = $this->orderRepository->getList(
            $this->searchCriteriaBuilder->addFilter('entity_id', $orderIds, 'in')->create()
        )->getItems();

        // Batch load all addresses in one query
        $orderEntityIds = array_map(fn($o) => (int) $o->getEntityId(), $orders);
        $addresses = $this->loadAddressesForOrders($orderEntityIds); // 1 query

        $result = [];
        foreach ($orders as $order) {
            $address = $addresses[$order->getEntityId()] ?? null;
            $result[] = [
                'id'    => $order->getId(),
                'total' => $order->getGrandTotal(),
                'city'  => $address?->getCity(),
            ];
        }
        return $result; // Total: 2 queries regardless of collection size
    }

    private function loadAddressesForOrders(array $orderIds): array
    {
        $connection = $this->resourceConnection->getConnection();
        $select = $connection->select()
            ->from(['a' => $this->resourceConnection->getTableName('sales_order_address')])
            ->where('a.parent_id IN (?)', $orderIds)
            ->where('a.address_type = ?', 'shipping');

        $rows = $connection->fetchAll($select);
        $indexed = [];
        foreach ($rows as $row) {
            $indexed[$row['parent_id']] = $row;
        }
        return $indexed;
    }
}

Blackfire – profiling in practice

# Install Blackfire agent and PHP probe in DDEV
ddev get ddev/ddev-blackfire

# Profile a specific URL
blackfire curl https://magento2-dev.ddev.site/catalog/product/view/id/42

# Profile a CLI command
blackfire run php bin/magento catalog:reindex

# Profile in PHPStorm via Blackfire plugin
# Set breakpoints -> Run with Blackfire -> See call graph

# Most common findings:
# 1. N+1 queries - visible as many identical DB calls in the call graph
# 2. Missing cache - same data fetched multiple times
# 3. Heavy serialization - large objects being serialized/deserialized
# 4. Missing indexes - slow DB queries with many rows examined

Database query log for N+1 detection

<?php

// Quick N+1 detection during development - count queries before and after a block
class QueryCounter
{
    private int $queryCount = 0;

    public function __construct(
        private \Magento\Framework\App\ResourceConnection $resource
    ) {
        // Enable query logging
        $this->resource->getConnection()->setProfiler(
            new \Zend_Db_Profiler(true)
        );
    }

    public function getCount(): int
    {
        return $this->resource->getConnection()
            ->getProfiler()
            ->getTotalNumQueries();
    }

    public function getQueries(): array
    {
        $profiler = $this->resource->getConnection()->getProfiler();
        $queries = [];
        foreach ($profiler->getQueryProfiles() as $profile) {
            $queries[] = $profile->getQuery();
        }
        return $queries;
    }
}

// Usage in a test or development helper
$before = $counter->getCount();
$result = $service->process($data);
$after  = $counter->getCount();
echo "Queries executed: " . ($after - $before) . "\n";

Summary

Magento 2 performance optimisation is most effective when approached in priority order. OPcache and Redis are low-effort, high-impact changes that should always be in place on production. N+1 queries in custom modules are the most common source of unexplained slowness – Blackfire makes them immediately visible. Fix the database layer first; infrastructure changes only help if the application is not generating unnecessary load.

About Henryk Tews

What you can read next

Xdebug – configuration, PHPStorm, debugging Magento plugins
Strategy pattern in PHP – and how Magento 2 uses it in pricing

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}