Jak zrychlit PrestaShop 1.6 s minimem úprav

Dlouhé roky pro zákazníky dělám rychlostní optimalizace webových aplikací. Nejčastěji se přitom jedná o různý Open Source software. V případě eshopu je to o to horší, že dlouhé načítání snadno odradí zákazníky od nákupu.

PrestaShop je v dnešní době asi nejlepším Open Source eshopem vůbec. To nic nemění na tom, že pokud to myslíte s online prodejem vážně, měli byste se mu obloukem vyhnout. Nebudu se tu dnes zabývat tím, proč tomu tak je. To je téma na jiný článek. Dnes si řekneme, jak s dobou jeho načítání jednoduše něco udělat pár úpravami, pokud jej již nasazen máte.

Často zákazníkům radím, že levnější než sahat do kódu je jít na to „hrubou silou™“. Tedy pořídit výkonnější server nebo VPS. V tomto případě ale ani přesun na výkonný server dobu načítání příliš nezkrátil. Chtěl jsem tedy zkusit, co se s tím dá udělat v rámci zákazníkova malého rozpočtu.

Tento návod je pro verzi PrestaShop 1.6.1.6, ale pravděpodobně bude fungovat i na ostatní verze PrestaShop 1.6. Vliv úprav jsem měřil pomocí nástroje Blackfire.io, který byl v odhalování nejhorších prohřešků velmi nápomocen. Veškeré testy byly prováděny s vypnutou cache, abych dostal relevantní výsledky. Také jsem se primárně zaměřil jen na výpis kategorie, který trval nejdéle. Provedené úpravy však často pomůžou i v jiné části shopu.

Něco málo o množství dat v databázi eshopu:

  • 95 900 produktů
  • 3 700 kategorií
  • 129 000 obrázků
  • Jen minimum variant
  • Jen minimum vlastností produktů
  • Bez příslušenství
  • 3 základní uživatelské skupiny
  • 4 jazyky
  • Aktivovaný modul „Blok Filtrování zboží dle parametrů“ (blocklayered)

VPS server, na kterém shop běžel:

  • 8 virtuálních jader
  • 16 GB paměti RAM
  • 240 GB místa na disku (RAID10, SATA 7200rpm)

Počáteční stav načítání kategorie před úpravami vypadal takto:

Jak zrychlit PrestShop 1.6 s minimem úprav 1

Problém 1

Modul blockcategories volá v metodě getTree() pro každou jednotlivou kategorii metodu Link::getCategoryLink() (soubor classes/Link.php). Nepředává jí ovšem instanci kategorie, ale jen její id a link_rewrite. V metodě Link::getCategoryLink() pak najdeme takovýto kód:

public function getCategoryLink($category, $alias = null, $id_lang = null, $selected_filters = null, $id_shop = null, $relative_protocol = false)
{
    if (!$id_lang) {
        $id_lang = Context::getContext()->language->id;
    }

    $url = $this->getBaseLink($id_shop, null, $relative_protocol).$this->getLangLink($id_lang, null, $id_shop);

    if (!is_object($category)) {
        $category = new Category($category, $id_lang);
    }

    // Set available keywords
    $params = array();
    $params['id'] = $category->id;
    $params['rewrite'] = (!$alias) ? $category->link_rewrite : $alias;
    $params['meta_keywords'] =    Tools::str2url($category->getFieldByLang('meta_keywords'));
    $params['meta_title'] = Tools::str2url($category->getFieldByLang('meta_title'));

    // Selected filters is used by the module blocklayered
    $selected_filters = is_null($selected_filters) ? '' : $selected_filters;

    if (empty($selected_filters)) {
        $rule = 'category_rule';
    } else {
        $rule = 'layered_rule';
        $params['selected_filters'] = $selected_filters;
    }

    return $url.Dispatcher::getInstance()->createUrl($rule, $id_lang, $params, $this->allow, '', $id_shop);
}

Tedy pro každou z 3700 kategorií se vytvoří instance třídy Category a její data se načtou z databáze! Také máte pocit, že se páni vývojáři PrestaShopu někde museli upsat? Jak by tedy metoda měla vypadat?

public function getCategoryLink($category, $alias = null, $id_lang = null, $selected_filters = null, $id_shop = null, $relative_protocol = false)
{
    if (!$id_lang) {
        $id_lang = Context::getContext()->language->id;
    }

    $url = $this->getBaseLink($id_shop, null, $relative_protocol).$this->getLangLink($id_lang, null, $id_shop);

    if (!is_object($category) && !$alias) {
        $category = new Category($category, $id_lang);
    }

    // Set available keywords
    $params = array();
    $params['id'] = is_object($category) ? $category->id : $category;
    $params['rewrite'] = (!$alias) ? $category->link_rewrite : $alias;
    // $params['meta_keywords'] =    Tools::str2url($category->getFieldByLang('meta_keywords'));
    // $params['meta_title'] = Tools::str2url($category->getFieldByLang('meta_title'));

    // Selected filters is used by the module blocklayered
    $selected_filters = is_null($selected_filters) ? '' : $selected_filters;

    if (empty($selected_filters)) {
        $rule = 'category_rule';
    } else {
        $rule = 'layered_rule';
        $params['selected_filters'] = $selected_filters;
    }

    return $url.Dispatcher::getInstance()->createUrl($rule, $id_lang, $params, $this->allow, '', $id_shop);
}

Co jsme upravili?

  • Zakomentovali jsme načtení kategorie do objektu a tím snížili nápor na databázi o 3700 SQL dotazů. Samotné instanciování třídy Category nám také dost času ušetří.
  • Upravili jsme řádek 13, aby se do pole $params správně nastavilo id kategorie v závislosti na tom, zda nám do metody přijde objekt, nebo jen id.
  • Zakomentovali jsme přiřazení parametrů meta_keywords a meta_title do pole $params. Dalším zkoumáním kódu jsem zjistil, že jsou potřeba jen v případě, že si je nastavíte do pole „Cesta ke kategoriím“ v nastavení eshopu. Vůbec mě nenapadá, proč by někdo něco takového chtěl dělat, když má k dispozici parametr rewrite 🙂

Poslední bod by samozřejmě šel řešit mnohem elegantněji, podobně jako řádek 13. Museli bychom však zajistit, že nám tyto parametry přijdou už na vstupu. To by znamenalo upravit veškeré SQL dotazy, jejichž výsledky vstupují do metody Link::getCategoryLink() napříč celým shopem. To by ovšem bylo na delší dobu a nebylo to tím pádem předmětem zakázky.

Jak tato jednoduchá úprava pomohla? Zkrátili jsme načítání kategorie o 4,6 vteřiny!

Jak zrychlit PrestShop 1.6 s minimem úprav 2

Problém 2

Modul blocktopmenu volá v metodě generateCategoriesMenu() (soubor modules/blocktopmenu/blocktopmenu.php) opět naši známou Link::getCategoryLink(), jen schovanou do metody Category::getLink(). Tentokrát ale nenechává instanciování třídy Category na metodě getCategoryLink(), ale vytváří ji sám. Zkrácený kód metody vypadá takto:

protected function generateCategoriesMenu($categories, $is_children = 0)
{
    $html = '';

    foreach ($categories as $key => $category) {
        if ($category['level_depth'] > 1) {
            $cat = new Category($category['id_category']);
            $link = Tools::HtmlEntitiesUTF8($cat->getLink());
        } else {
            $link = $this->context->link->getPageLink('index');
        }

        // kód zkrácen
    }

    return $html;
}

Přitom potřebný link_rewrite ve výsledku SQL dotazu již máme. Můžeme tedy jednoduše instanciování třídy Category opět vynechat a předat metodě Link::getCategoryLink() pouze potřebné údaje:

protected function generateCategoriesMenu($categories, $is_children = 0)
{
    $html = '';

    foreach ($categories as $key => $category) {
        if ($category['level_depth'] > 1) {
            //$cat = new Category($category['id_category']);
            //$link = Tools::HtmlEntitiesUTF8($cat->getLink());
            $link = $this->context->link->getCategoryLink($category['id_category'], $category['link_rewrite']);
        } else {
            $link = $this->context->link->getPageLink('index');
        }

        // kód zkrácen
    }

    return $html;
}

Jak nám tato úprava pomohla? Zkrátili jsme načítání kategorie o dalších 700 ms! Není to již tolik jako předtím, ale dostáváme se už na vcelku přijatelné 2 vteřiny dvěma jednoduchými úpravami.

Jak zrychlit PrestShop 1.6 s minimem úprav 3

Problém 3

Modul blockcategories vybírá z databáze v metodě hookFooter() (soubor modules/blockcategories/blockcategories.php) celý strom kategorií. Přitom v šabloně, kterou zákazník použil, jsou v patičce stránky vypsány jen top-level kategorie. Je tedy trochu zbytečné vybírat kategorie všechny. Co myslíte? Můžeme tedy řádek:

$maxdepth = Configuration::get('BLOCK_CATEG_MAX_DEPTH');

snadno upravit takto:

$maxdepth = 2;

Pokud máte v patičce stránky i podkategorie, upravte hodnotu $maxdepth dle vlastního uvážení a potřebné hloubky.

Jak nám tato úprava pomohla? Snížili jsme načítání kategorie o dalších 600 ms!

Jak zrychlit PrestShop 1.6 s minimem úprav 4

Nyní už se výpis kategorie načítá vcelku přijatelných 1,39 vteřin. Moje práce pro zákazníka tedy po 2,5 hodině ladění končí. Dalo by se jistě pokračovat mnohem dál. A nejen na výpisu kategorie, ale i jiných stránkách, které mají zase svoje vlastní specifika.

Pro zajímavost ještě ukážu, jak rychle se kategorie načte po zapnutí cache:

Jak zrychlit PrestShop 1.6 s minimem úprav 5

Výkonovému ladění zdar!

15 komentářů u „Jak zrychlit PrestaShop 1.6 s minimem úprav

  1. Aleš

    Dobrý den,

    Pokouším se udělat úpravu kodu, a hned jsem narazil na to že nevím v jkých souborech mám upravit PHP kod. Můžete mi prosím zdělit názvy souborů pro úpravy v bodech 1 až 3?

    děkuji

    Růžička

    1. Tomáš Jacík Autor příspěvku

      V článku vždy popisuji, ve kterém modulu nebo třídě daná funkce je.

      1. Link::getCategoryLink() – soubor classes/Link.php
      2. generateCategoriesMenu() – soubor modules/blocktopmenu/blocktopmenu.php
      3. hookFooter() – soubor modules/blockcategories/blockcategories.php

  2. Veronika

    Díky za super článek, moc mi pomohl. Ale přiznám se, že první problém jsem taky hledala v jiném souboru – blockcategories.php, ale komentář pod článkem pomohl. Můj web se zrychlil o více něž sekundu a hodnocení googlu pro mobily se zlepšilo na dvojnásobek! Ještě jednou díky moc, tomu říkám užitečný článek 😉

  3. d@rkWolf

    Zdravím, vzhledem k tomu, že mě na tento blogpost navedl šéf, protože stále hledáme, jak je možné PS urychlit, tak jsem se tomu zkusil trochu věnovat a narazil sem na pár problémů. Všechno se týká problému č.1, 2-nemáme nainstalováno, 3-nemáme kategorie ve footeru(jen cms stránky):

    1. myslím/doufám, že většina uživatelů PS nepoužívá zobrazení, aby měli v blockcategories všechny existující kategorie(a ještě takto šílené množství).
    2. úprava spoléhá na deprecated vstup do funkce getCategoryLink – viz.: @param mixed $category Category object (can be an ID category, but deprecated), což znamená kontroly při aktualizaci eshopu, může být kdykoliv odstraněno)
    3. po vyřazení objektu se začne hned na dalším řádku po úpravě(14-params rewrite) logovat notice o přístupu k „non-object“, v případě, že není nastaven alias-což bude !asi! běžné, ten alias je spíš pro ruční generování linků, jestli to dobře chápu
    Upravil jsem ho tedy takto:
    $params[‚rewrite‘] = (!$alias) ? (is_object($category) ? $category->link_rewrite : “ ) : $alias;
    (samozřejmě ještě vhodnější bude to obalit celé podmínkou is_object, místo abych ju opakoval)

    Čímž vyřeším problém s přístupem k neexistujícímu objektu, ale pořád v $params postrádám všude tam, kde není vytvořen objekt údaj „rewrite“, který nevím, kdy a za jakých podmínek je potřebný a kdy může způsobit chybu.

    Zajímalo by mě, jestli jste toto testoval, jestli chybějící rewrite(který tam prostě bez vytvořeného objektu pro každou kategorii takto nebude) kdekoliv v eshopu něco nerozbíjí?

    1. Tomáš Jacík Autor příspěvku

      1. Výpis kategorií je často omezen na nějaký level. Běžně se používají 3 nebo 4 úrovně. I při tomto nastavení je to dost odkazů.
      2. Ano, máte pravdu. Na začátku jsem psal, pro kterou verzi PS úpravy jsou. Tohle neměl být 100% návod na všechny verze a problémy, spíše případová studie.
      3. Je to možné. Záleží na verzi PS a na šabloně. Použití funkce getCategoryLink() se může lišit. Neměl by však být problém použití bez aliasu dohledat a opravit. Většina SQL dotazů link_rewrite stejně vytahuje, i když jej pak nepoužívá. Pointa úpravy funkce getCategoryLink() byla v tom, aby se nedělal dotaz do databáze pro každý jednotlivý odkaz.

      Ano, testoval jsem na tomto konkrétním eshopu, kde jsem úpravy dělal. Všude se odkazy generovaly správně a do logu mi žádné chyby nepadaly. Mrzí mě, pokud Vám dané úpravy nefungují. Mělo to však být spíše nakopnutí, že velké problémy s výkonem lze vyřešit jednoduše. Před úpravami také doporučuji použít nástroj Blackfire.io, který Vám prozradí, kde na Vašem konkrétním eshopu máte největší problémy. Ono se to podle rozložení objektů v databázi může dost lišit.

    1. Tomáš Jacík Autor příspěvku

      Pokud jde o seriózní obchodní záměr a ne jen otestování trhu, vždy doporučuji řešení na míru postavené na základě uživatelského výzkumu.

      1. Tomáš Jacík Autor příspěvku

        Anoj, je to služba blackfire.io, která je na ladění výkonu velice užitečná. Pro monitoring výkonu v produkci také používám New Relic.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *