• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to Port Performance-Critical Parts of Magento 1 to Magento 2 Safely

How to Port Performance-Critical Parts of Magento 1 to Magento 2 Safely

Identifying Performance Bottlenecks in Magento 1

Before embarking on any migration, a thorough understanding of your Magento 1 application’s performance profile is paramount. This isn’t about general Magento slowness; it’s about pinpointing the specific modules, database queries, or API calls that consume disproportionate resources. We’ll focus on areas that are likely to be “performance-critical” and thus require careful porting rather than a complete rewrite.

Common culprits include:

  • Heavy custom module logic, especially in observers triggered on core events like sales_order_save_after or catalog_product_save_after.
  • Complex product collection loading with numerous EAV attributes, filters, or custom joins.
  • Third-party integrations performing synchronous, blocking operations during checkout or product view.
  • Inefficient database queries, often due to missing indexes or N+1 query patterns.
  • Custom indexing processes or data manipulation scripts.

Tools like New Relic, Blackfire.io, or even Magento’s built-in profiler (though less granular) are essential for this phase. Focus on identifying slow controller actions, inefficient collection fetches, and excessive database query counts per request.

Strategic Approaches to Porting Performance-Critical Code

Directly translating Magento 1’s monolithic structure and reliance on older PHP patterns to Magento 2’s more modular, service-oriented architecture (SOA) is rarely optimal. We need a strategy that leverages Magento 2’s strengths while minimizing the risk of introducing new performance issues.

1. Re-architecting with Magento 2 Service Contracts and Repositories

Magento 1’s data access often involved direct calls to models and resource models, leading to tight coupling and difficulty in optimizing. Magento 2’s Service Contracts and Repositories pattern provides a cleaner abstraction. Performance-critical logic that heavily interacts with data should be refactored to use these.

Consider a Magento 1 module that fetches product data with custom logic:

// Magento 1 Example (Simplified)
class My_Module_Model_Product_Fetcher extends Mage_Core_Model_Abstract
{
    public function getSpecialProducts($limit = 10)
    {
        $collection = Mage::getModel('catalog/product')->getCollection()
            ->addAttributeToSelect('*')
            ->addAttributeToFilter('special_price', array('notnull' => true))
            ->setPageSize($limit);

        // Custom logic here...
        foreach ($collection as $product) {
            // ... more processing
        }
        return $collection;
    }
}

In Magento 2, this would ideally be implemented using a Service Contract and a Repository. The repository would encapsulate the data fetching logic, potentially using optimized collection methods or even direct database queries if necessary, abstracting the complexity away from the business logic.

// Magento 2 Service Contract Interface
namespace Vendor\Module\Api\Product;

interface SpecialProductRepositoryInterface
{
    /**
     * @param int $limit
     * @return \Vendor\Module\Api\Data\ProductInterface[]
     */
    public function getSpecialProducts(int $limit = 10): array;
}

// Magento 2 Repository Implementation (Conceptual)
namespace Vendor\Module\Model\Product;

use Vendor\Module\Api\Product\SpecialProductRepositoryInterface;
use Magento\Catalog\Api\ProductRepositoryInterface; // Standard Magento repository
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use Magento\Framework\Api\SearchCriteriaBuilder;

class SpecialProductRepository implements SpecialProductRepositoryInterface
{
    private $productRepository;
    private $productCollectionFactory;
    private $searchCriteriaBuilder;

    public function __construct(
        ProductRepositoryInterface $productRepository,
        ProductCollectionFactory $productCollectionFactory,
        SearchCriteriaBuilder $searchCriteriaBuilder
    ) {
        $this->productRepository = $productRepository;
        $this->productCollectionFactory = $productCollectionFactory;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
    }

    public function getSpecialProducts(int $limit = 10): array
    {
        $collection = $this->productCollectionFactory->create();
        $collection->addAttributeToSelect('*');
        // Magento 2 uses EAV attributes differently, often via attribute codes
        $collection->addAttributeToFilter('special_price', ['notnull' => true]);
        $collection->setPageSize($limit);
        $collection->load(); // Load the collection

        // Convert to API Data Objects if necessary
        $products = [];
        foreach ($collection->getItems() as $item) {
            // Map Magento 2 product object to your API Data Object
            // This mapping itself could be a performance consideration
            $products[] = $this->mapToApiData($item);
        }
        return $products;
    }

    private function mapToApiData($product): \Vendor\Module\Api\Data\ProductInterface
    {
        // Implementation to map Magento product to Vendor\Module\Api\Data\ProductInterface
        // This is where custom attribute mapping or logic would reside.
        // For performance, consider lazy loading or only fetching required attributes.
        return $product; // Placeholder
    }
}

The key here is that the repository encapsulates the data retrieval. If the underlying query needs optimization (e.g., specific indexes, avoiding EAV lookups where possible), it’s done within the repository, not scattered across controllers or observers.

2. Migrating Observers to Plugins (Interceptors)

Magento 1’s observer pattern, while flexible, can lead to a long chain of executed code for a single event, impacting performance. Magento 2’s plugin system offers more control and predictability. For performance-critical observers in M1, consider if they can be replaced by M2 plugins (before, after, around).

An M1 observer that modifies order data:

// Magento 1 Observer
class My_Module_Model_Observer
{
    public function addCustomDataToOrder(Varien_Event_Observer $observer)
    {
        $order = $observer->getEvent()->getOrder();
        // Complex logic to fetch and set custom data
        $customValue = $this->fetchCustomValue($order->getCustomerId());
        $order->setMyCustomField($customValue);
        // Potential for multiple database writes or external API calls here
    }

    private function fetchCustomValue($customerId)
    {
        // ... expensive operation
        return 'some_value';
    }
}

In Magento 2, this could be an ‘around’ plugin on the `placeOrder` method of the `SalesOrderManagementInterface` or a ‘before’/’after’ plugin on the `save` method of a relevant repository. An ‘around’ plugin offers the most control, allowing you to bypass the original method entirely if your logic dictates.

// Magento 2 Plugin (Conceptual)
namespace Vendor\Module\Plugin\Sales\Order;

use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderManagementInterface; // Example interface

class OrderManagementPlugin
{
    private $customDataService; // Your service to fetch custom data

    public function __construct(
        \Vendor\Module\Service\CustomDataService $customDataService
    ) {
        $this->customDataService = $customDataService;
    }

    /**
     * Around plugin to potentially modify or bypass the original method.
     *
     * @param OrderManagementInterface $subject
     * @param callable $proceed
     * @param OrderInterface $order
     * @return OrderInterface
     */
    public function aroundPlaceOrder(
        OrderManagementInterface $subject,
        callable $proceed,
        OrderInterface $order
    ): OrderInterface {
        // Perform custom logic BEFORE calling the original method
        $customValue = $this->customDataService->fetchCustomValue($order->getCustomerId());
        // You might set this on the order object if it's a valid attribute,
        // or pass it along in a modified order object if the method signature allows.
        // For simplicity, let's assume we can modify the order object passed in.
        // Note: Modifying input parameters in 'around' plugins requires careful consideration.
        // A 'before' plugin is often safer for modifying input.

        // Call the original method
        $result = $proceed($order);

        // Perform custom logic AFTER the original method has executed
        // $result is the order object returned by the original method.
        // You might fetch additional data based on the newly created order ID.

        return $result;
    }

    // Example of a 'before' plugin on a save method (more common for modifying input)
    // public function beforeSave(OrderRepository $subject, OrderInterface $order)
    // {
    //     $customValue = $this->customDataService->fetchCustomValue($order->getCustomerId());
    //     $order->setMyCustomField($customValue); // Assuming MyCustomField is a valid attribute
    //     return [$order]; // Must return arguments
    // }
}

Plugins offer better performance characteristics than observers because Magento 2’s dependency injection and AOP (Aspect-Oriented Programming) can optimize plugin execution. ‘Around’ plugins are powerful but should be used judiciously, as they can obscure the original logic if overused. For simple data modifications, ‘before’ or ‘after’ plugins are generally preferred.

3. Optimizing Database Interactions

Magento 1’s EAV model is notorious for performance issues, especially with large catalogs. Magento 2 improves this with better indexing and a more structured approach, but custom code can still create bottlenecks.

3.1. Refactoring EAV Queries

Identify M1 code that performs many EAV attribute lookups. In M2, try to fetch only necessary attributes using `addAttributeToSelect()` and leverage the `ProductRepositoryInterface` which is optimized for fetching specific product data.

// Magento 1: Potentially slow due to multiple EAV lookups per product
$collection = Mage::getModel('catalog/product')->getCollection();
foreach ($collection as $product) {
    $price = $product->getPrice(); // EAV lookup
    $name = $product->getName();   // EAV lookup
    // ... many more attribute lookups
}

// Magento 2: More efficient, fetches only specified attributes
// Using the standard ProductRepository
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
$searchCriteria = $this->searchCriteriaBuilder->create();
$products = $productRepository->getList($searchCriteria)->getItems();

foreach ($products as $product) {
    // Attributes selected via addAttributeToSelect() or default attributes are faster
    $price = $product->getPrice();
    $name = $product->getName();
    // ...
}

// For extreme performance, consider direct SQL or custom index tables
// if standard collection loading is still a bottleneck.

3.2. Avoiding N+1 Query Problems

This is a universal database problem. In M1, it often manifested when iterating over a collection and then performing a separate database query for each item (e.g., fetching related data). Magento 2’s ORM and collection loading mechanisms are generally better, but custom code can still introduce this. Use M2’s collection loading methods (`join`, `joinAttribute`, `addFilterToMap`) and ensure related data is fetched in a single query where possible.

// Magento 1: Potential N+1 if 'getCustomData' makes a DB call per product
$collection = Mage::getModel('catalog/product')->getCollection();
foreach ($collection as $product) {
    $customData = $product->getCustomData(); // Assume this makes a DB call
    // ...
}

// Magento 2: Use collection joins or repository methods that pre-fetch data
// Example using a custom collection with a join
class My_Product_Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
{
    public function addCustomDataToSelect()
    {
        $this->joinField(
            'custom_data_field', // Alias for the joined field
            'your_custom_table', // The table to join
            'custom_value_column', // The column to select from the joined table
            'entity_id = {{table}}.product_id', // Join condition
            'left' // Join type
        );
        return $this;
    }
}

// Usage in a Repository or Service
public function getProductsWithCustomData($limit = 10)
{
    $collection = $this->myProductCollectionFactory->create();
    $collection->addAttributeToSelect('*');
    $collection->addCustomDataToSelect(); // Add the custom data column
    $collection->setPageSize($limit);
    $collection->load();

    $products = [];
    foreach ($collection->getItems() as $item) {
        // $item->getCustomDataField() will now be populated from the join
        $products[] = $item;
    }
    return $products;
}

4. Migrating External API Integrations

Performance-critical integrations in M1 often involved synchronous calls within controllers or observers. In M2, these should be moved to asynchronous processing where possible (e.g., using Message Queues) or optimized to run outside the request-response cycle.

If a synchronous call is unavoidable, ensure it’s highly optimized:

  • Use efficient data serialization (e.g., Protobuf instead of JSON if supported by the external API).
  • Implement robust caching for API responses.
  • Use connection pooling if making many calls to the same endpoint.
  • Consider using Guzzle’s async capabilities within Magento 2 services.
// Magento 2: Using Guzzle for asynchronous API calls
namespace Vendor\Module\Service;

use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;

class ExternalApiService
{
    private $httpClient;
    private $jsonSerializer; // Assuming you have a serializer

    public function __construct(
        ClientInterface $httpClient,
        \Vendor\Module\Service\JsonSerializer $jsonSerializer
    ) {
        $this->httpClient = $httpClient;
        $this->jsonSerializer = $jsonSerializer;
    }

    /**
     * Sends a request asynchronously.
     * @param string $endpoint
     * @param array $data
     * @return PromiseInterface
     */
    public function sendAsyncRequest(string $endpoint, array $data): PromiseInterface
    {
        $options = [
            'headers' => [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ],
            'body' => $this->jsonSerializer->serialize($data),
        ];

        return $this->httpClient->requestAsync('POST', $endpoint, $options);
    }

    /**
     * Example of processing multiple async requests.
     */
    public function processMultipleRequests(array $requestsData): array
    {
        $promises = [];
        foreach ($requestsData as $data) {
            $promises[] = $this->sendAsyncRequest($data['endpoint'], $data['payload']);
        }

        // Wait for all promises to complete
        $responses = \GuzzleHttp\Promise\all($promises)->wait();

        $results = [];
        foreach ($responses as $index => $response) {
            $results[$index] = $this->jsonSerializer->unserialize((string) $response->getBody());
        }
        return $results;
    }
}

For truly performance-critical, synchronous operations that *must* happen during a user request (e.g., payment gateway interaction), ensure the external API is highly available and responsive. If it’s not, consider alternative providers or architectural changes to decouple your system from its latency.

Testing and Validation

Post-migration, rigorous performance testing is non-negotiable. Use the same profiling tools you used in step 1 to compare M1 and M2 performance for the ported functionalities.

  • Load Testing: Simulate realistic user traffic to identify bottlenecks under load. Tools like JMeter or k6 are invaluable.
  • Profiling: Use Blackfire.io or New Relic to analyze execution time, memory usage, and database queries for specific critical paths.
  • Benchmarking: Measure response times for key operations (e.g., product view, add to cart, checkout steps) and compare against M1 baselines.
  • Database Query Analysis: Use `EXPLAIN` on critical SQL queries identified during profiling. Ensure appropriate indexes are in place.

Iterate on the code based on profiling results. Often, initial porting provides functional parity, but performance tuning requires a second pass, focusing on the specific areas highlighted by your testing.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing thread pools deadlock during concurrent ActiveRecord transaction processing on Linode Servers
  • Securing Your E-commerce APIs: Preventing SQL Injection (SQLi) in customized checkout queries in WooCommerce Implementations
  • Disaster Recovery 101: Architecting Auto-Failovers for MySQL and Ruby Deployments on Linode
  • High-Throughput Caching Strategies: Scaling MySQL for Perl Application APIs
  • Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Laravel Deployments on DigitalOcean

Copyright © 2026 · Vinay Vengala