Magento performance

In my previous article I wrote about performance issues when you have many attribute options for configurable attribute. Here I will provide you with several tips on how to increase performance if you have configurable products with many associated products (100 associated products and more).

There are 2 possible performance issues with such products:
1. Loading time of Product listing page is more than 10 seconds.
2. Loading time of Product details page is more than 10 seconds.

If you enable Magento Profiler you will see that in both cases the Mage_Catalog_Model_Product_Type_Configurable::getUsedProducts method takes a lot of time.

On the product page this method is required to collect all options/prices for configurable product. But in the product list page this method used only in the Mage_Catalog_Model_Product_Type_Configurable::isSalable method to check if the product available for sale or not.

So you need to change the isSalable method:

    public function isSalable($product = null)
    {
        $salable = parent::isSalable($product);

        if ($salable !== false) {
            $salable = false;
            if (!is_null($product)) {
                $this->setStoreFilter($product->getStoreId(), $product);
            }
            foreach ($this->getUsedProducts(null, $product) as $child) {
                if ($child->isSalable()) {
                    $salable = true;
                    break;
                }
            }
        }

        return $salable;
    }

To something like this:

public function isSalable($product = null)
    {
        $salable = parent::isSalable($product);

        if ($salable !== false) {
            $salable = false;
            if (!is_null($product)) {
                $this->setStoreFilter($product->getStoreId(), $product);
            }

            if (!Mage::app()->getStore()->isAdmin() && $product) {
                $collection = $this->getUsedProductCollection($product)
                    ->addAttributeToFilter('status', Mage_Catalog_Model_Product_Status::STATUS_ENABLED)
                    ->setPageSize(1)
                    ;
                if ($collection->getFirstItem()->getId()) {
                    $salable = true;
                }
            } else {
                foreach ($this->getUsedProducts(null, $product) as $child) {
                    if ($child->isSalable()) {
                        $salable = true;
                        break;
                    }
                }
            }
        }

        return $salable;
    }

In our case we try to find first associated product with Enabled status, and it is OK for us. By default Magento also checks the stock status.

After this modification your product listing page should load approximately with the same time as other pages of the store.

To solve issue on the Product details page we need to cache products loading. You can do it by changing the Mage_Catalog_Model_Product_Type_Configurable::getUsedProducts method:

public function getUsedProducts($requiredAttributeIds = null, $product = null)
    {
        Varien_Profiler::start('CONFIGURABLE:'.__METHOD__);
        if (!$this->getProduct($product)->hasData($this->_usedProducts)) {
            if (is_null($requiredAttributeIds)
                and is_null($this->getProduct($product)->getData($this->_configurableAttributes))) {
                // If used products load before attributes, we will load attributes.
                $this->getConfigurableAttributes($product);
                // After attributes loading products loaded too.
                Varien_Profiler::stop('CONFIGURABLE:'.__METHOD__);
                return $this->getProduct($product)->getData($this->_usedProducts);
            }

            $use_cache = false;
            if (!Mage::app()->getStore()->isAdmin() && $product) {
                if (!is_dir(Mage::getBaseDir('cache') . DS . 'associated')) {
                    @mkdir(Mage::getBaseDir('cache') . DS . 'associated', 0777);
                }

                $file_cache = Mage::getBaseDir('cache') . DS . 'associated' . DS . $product->getId() . '.php';
                $use_cache = true;
            }

            $usedProducts = array();
            if ($use_cache && is_file($file_cache)) {
                $data = include_once($file_cache);
                foreach ($data as $k => $d) {
                    $class_name = Mage::getConfig()->getModelClassName('catalog/product');
                    $usedProducts[] = new $class_name($d);
                }
            } else {
                $collection = $this->getUsedProductCollection($product)
                    ->addAttributeToSelect('*')
                    ->addFilterByRequiredOptions();

                if (is_array($requiredAttributeIds)) {
                    foreach ($requiredAttributeIds as $attributeId) {
                        $attribute = $this->getAttributeById($attributeId, $product);
                        if (!is_null($attribute))
                            $collection->addAttributeToFilter($attribute->getAttributeCode(), array('notnull'=>1));
                    }
                }

                if ($use_cache) {
                    $cache_str = '';
                    foreach ($collection as $id => $item) {
                        $item->unsetData('stock_item');
                        $cache_str .= $id . " => " . var_export($item->getData(), true) . ",\n";
                        $usedProducts[] = $item;
                    }

                    $fd = fopen($file_cache, 'wb+');
                    fwrite($fd, "<?php\nreturn array(" . $cache_str . ");\n?>");
                    fclose($fd);
                } else {
                    foreach ($collection as $item) {
                        $usedProducts[] = $item;
                    }
                }
            }

            $this->getProduct($product)->setData($this->_usedProducts, $usedProducts);
        }
        Varien_Profiler::stop('CONFIGURABLE:'.__METHOD__);
        return $this->getProduct($product)->getData($this->_usedProducts);
    }

In the code we checking the possibility to use cache. Also we create folder for cache files var/cache/associated/ (if it does not exists). The cache file name is {product_id}.php.

If we already have a cache file we just take information from it. We use php code in cache - it is an array with product data, so our cache file is valid PHP file.
We assign product data to Product model (without loading) from the cache. Note that we use current product model from config:

$class_name = Mage::getConfig()->getModelClassName('catalog/product');
$usedProducts[] = new $class_name($d);

We use standard PHP function var_export() for cache file creation.

Sometimes you will need to cache stock_item data - it is an object so it should be exported like product object, and should be assign to product when you load the cache. In our case it is not necessary.

When you complete all steps I described, the product page loading time will be much less.

The last thing is to clear the cache, to do it, just remove the cache file in the Mage_Catalog_Model_Product_Type_Configurable::save() method:

if (is_file(Mage::getBaseDir('cache') . DS . 'associated' . DS . $product->getId() . '.php')) {
    unlink(Mage::getBaseDir('cache') . DS . 'associated' . DS . $product->getId() . '.php');
}

We completed a lot of Magento speed up investigations and found that most of performance issues are connected with third party extensions which modify attributes/custom options functionality. There is no any ideal solution for all stores as each situation is unique and should be investigated.

You can always use Magento profiler to find critical performance issues.

We recommend to avoid creation of configurable products with many associations (1000+), and if possible split such configurable products to several ones.