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:
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!
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.
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!
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:
Výkonovému ladění zdar!