Jak optimalizovat webovou aplikaci PHP Laravel pro vysoký výkon?

Laravel je komplexní framework, ale rychlost není jeho silnou stránkou. Podívejme se na několik technik, jak ho zrychlit!

V současnosti se jen málo PHP vývojářů vyhne kontaktu s Laravelem. Ať už jde o juniory a medior vývojáře, kteří si cení rychlého vývoje, nebo o zkušené programátory, které k němu tlačí trh, Laravel je všudypřítomný.

Nelze popřít, že Laravel oživil PHP ekosystém (osobně bych PHP opustil už dávno, kdyby nebylo Laravelu).

Zde je úryvek (poněkud oprávněné) chvály od samotného Laravelu.

Laravel se snaží usnadnit vývojářům práci, což znamená, že pod kapotou odvádí spoustu práce, aby zajistil pohodlí. Všechny „magické“ funkce Laravelu, které se zdají fungovat automaticky, mají za sebou vrstvy kódu, které se spouštějí při každém použití. I jednoduchá výjimka ukazuje, jak hluboko tato „králičí nora“ sahá (všimněte si, jak se chyba dostane až k samotnému jádru).

Například při zdánlivé chybě v pohledu existuje 18 volání funkcí, než se dostaneme k podstatě problému. Osobně jsem zažil i 40 volání a pokud používáte různé knihovny a pluginy, může jich být ještě víc.

Zkrátka, tyto vrstvy kódu ve výchozím nastavení Laravel zpomalují.

Jak moc je tedy Laravel pomalý?

Upřímně řečeno, na tuto otázku není snadné odpovědět z několika důvodů.

Za prvé, neexistuje žádný obecně uznávaný objektivní standard pro měření rychlosti webových aplikací. Rychlejší nebo pomalejší oproti čemu? A za jakých podmínek?

Za druhé, webová aplikace závisí na mnoha faktorech (databáze, souborový systém, síť, cache atd.), takže je těžké hovořit o rychlosti obecně. Velmi rychlá aplikace s pomalou databází je prostě pomalá.

Právě tato nejistota ale vede k oblíbenosti benchmarků. I když o nich lze pochybovat (viz tento a tento), poskytují určitý referenční rámec. Proto, s určitou dávkou skepse, si můžeme udělat hrubou představu o rychlosti PHP frameworků.

Podívejme se na poměrně slušný GitHub zdroj, kde jsou frameworky porovnávány:

Laravel zde stěží uvidíte, pokud se opravdu nesoustředíte na konec žebříčku. Ano, Laravel je poslední! Většina těchto „frameworků“ není moc praktická, ale ukazuje, jak pomalý Laravel ve srovnání s ostatními může být.

V běžných aplikacích si tuto „pomalost“ většinou neuvědomíme, protože naše webové aplikace zřídka čelí vysoké návštěvnosti. Ale jakmile k tomu dojde (například 200-500 souběžných požadavků), servery se začnou dusit a selhávat. V té chvíli už přidání dalšího hardwaru nepomůže a náklady na infrastrukturu rychle rostou.

Ale nezoufejte! Tento článek není o tom, co se nedá, ale o tom, co dělat můžete. 🙂

Dobrá zpráva je, že můžete udělat spoustu věcí, aby vaše Laravel aplikace běžela mnohem rychleji. Ano, opravdu. Můžete zrychlit kód a ušetřit stovky dolarů měsíčně za infrastrukturu. Jak na to? Jdeme na to.

Čtyři typy optimalizací

Optimalizaci lze provádět na čtyřech úrovních (pokud jde o PHP aplikace):

  • Úroveň jazyka: Používání rychlejší verze PHP a vyhýbání se pomalým konstrukcím.
  • Úroveň frameworku: Tím se budeme zabývat v tomto článku.
  • Úroveň infrastruktury: Ladění PHP-FPM, web serveru, databáze atd.
  • Úroveň hardware: Přechod k lepšímu a rychlejšímu poskytovateli hostingu.

Všechny tyto typy optimalizací mají své místo, ale tento článek se zaměří pouze na optimalizace typu 2: ty, které se týkají frameworku.

Mimochodem, toto dělení není žádný standard, jen jsem si to vymyslel. Neříkejte pak: „Potřebujeme optimalizaci typu 3“, váš vedoucí týmu by vás mohl zabít, a pak mě taky! 😀

A nyní se konečně dostáváme k samotným technikám.

N+1 databázové dotazy

Problém N+1 dotazů je při použití ORM běžný. Laravel má svůj Eloquent ORM, který je sice pohodlný, ale snadno se zapomeneme dívat na to, co se děje pod kapotou.

Představte si běžný scénář: zobrazení seznamu všech objednávek od daných zákazníků. To se často vyskytuje v e-commerce systémech a v různých reportech, kde se zobrazují data spojená s jinými entitami.

V Laravelu může vypadat funkce kontroleru takto:

class OrdersController extends Controller
{
    // ...

    public function getAllByCustomers(Request $request, array $ids) {
        $customers = Customer::findMany($ids);
        $orders = collect(); // nová kolekce

        foreach ($customers as $customer) {
            $orders = $orders->merge($customer->orders);
        }

        return view('admin.reports.orders', ['orders' => $orders]);
    }
}

Vypadá to elegantně, že? 🤩🤩

Bohužel, je to katastrofální způsob psaní kódu v Laravelu.

Proč?

Když požádáme ORM, aby vyhledal zákazníky, vygeneruje SQL dotaz:

SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);

To je v pořádku. Všechny výsledné řádky se uloží do kolekce $customers.

Nyní pro každého zákazníka získáme jeho objednávky. To vyvolá další dotaz…

SELECT * FROM orders WHERE customer_id = 22;

… a tolikrát, kolik je zákazníků.

Pokud tedy chceme objednávky pro 1000 zákazníků, celkový počet dotazů bude 1 (pro zákazníky) + 1000 (pro objednávky) = 1001. Odtud název N+1.

Lze to zlepšit? Ano! Použitím takzvaného eager loading (dychtivé načítání), můžeme přimět ORM, aby udělal JOIN a vrátil všechna data v jednom dotazu!

$orders = Customer::findMany($ids)->with('orders')->get();

Výsledná datová struktura je sice vnořená, ale data objednávek lze snadno extrahovat. Jediný dotaz bude vypadat přibližně takto:

SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);

Jeden dotaz je samozřejmě lepší než tisíc dotazů. Představte si, jak by to vypadalo, kdybychom chtěli zpracovat 10 000 zákazníků! Nebo nedej bože, kdybychom chtěli zobrazit položky v každé objednávce! Takže nezapomeňte, dychtivé načítání je téměř vždy dobrý nápad.

Cache konfigurace

Jedním z důvodů flexibility Laravelu je velké množství konfiguračních souborů, které definují chování frameworku. Chcete změnit, kam se ukládají obrázky? Stačí upravit soubor config/filesystems.php. Chcete pracovat s více ovladači front? Popište je v config/queue.php. Existuje 13 konfiguračních souborů, které pokrývají různé aspekty frameworku.

Při každém webovém požadavku se Laravel probudí, nabootuje a analyzuje všechny tyto konfigurační soubory. To je zbytečné, pokud se nic nezměnilo! Rebuilding konfigurace při každém požadavku je neefektivní a lze se tomu vyhnout pomocí příkazu:

php artisan config:cache

Tento příkaz sloučí všechny konfigurační soubory do jednoho a uloží ho do cache. Při dalším požadavku Laravel jen přečte tento jediný soubor.

Ale buďte opatrní, cachování konfigurace je citlivá operace. Největší problém je, že po spuštění tohoto příkazu, volání funkce `env()` (kromě konfiguračních souborů) vrátí `null`!

Dává to smysl. Pokud cachujete konfiguraci, říkáte frameworku: „Myslím, že mám vše nastaveno a nechci, aby se to měnilo.“ Očekáváte, že prostředí zůstane statické, k čemuž slouží soubory `.env`.

Zde jsou zásady pro cachování konfigurace:

  • Používejte to jen v produkčním prostředí.
  • Používejte to, jen když jste si opravdu jisti, že nechcete měnit konfiguraci.
  • Pokud se něco pokazí, zrušte nastavení pomocí `php artisan cache:clear`
  • Modlete se, ať škody pro firmu nejsou velké!

Omezte automatické načítání služeb

Pro svou funkčnost Laravel po startu načte spoustu služeb. Ty jsou uvedeny v `config/app.php` v poli `providers`. Podívejme se na příklad:

    /*
    |--------------------------------------------------------------------------
    | Autoloaded Service Providers
    |--------------------------------------------------------------------------
    |
    | The service providers listed here will be automatically loaded on the
    | request to your application. Feel free to add your own services to
    | this array to grant expanded functionality to your applications.
    |
    */

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

    ],

V seznamu je 27 služeb! Možná je všechny potřebujete, ale je to nepravděpodobné.

Například, pokud vytváříte REST API, nepotřebujete poskytovatele služeb relace, zobrazení atd. A protože máte vlastní postupy, a ne ty výchozí, možná nepotřebujete ani poskytovatele autentizace, stránkování a překladů. Celkově je skoro polovina z nich zbytečná.

Pečlivě zvažte, zda je všechny potřebujete. A hlavně, nekometujte je slepě a neposílejte do produkce! Proveďte testy, zkontrolujte vše ručně a buďte paranoidní, než cokoliv změníte.

Mějte na paměti middleware

Když potřebujete vlastní zpracování webového požadavku, řešením je middleware. Můžete ho umístit do `app/Http/Kernel.php` do zásobníku webu nebo API. Pak bude dostupný v celé aplikaci, pokud nedělá nic, co by narušovalo běh (například logování).

Jak se ale aplikace rozrůstá, tento globální middleware se může stát zátěží, pokud se spouští při každém požadavku, i když pro to není důvod.

Buďte opatrní, kam middleware přidáváte. I když je globální middleware pohodlný, výkon se tím snižuje. Chápu, že by selektivní aplikace mohla být nepříjemná, ale je to něco, co doporučuji.

Občas se vyhněte ORM

Eloquent sice usnadňuje práci s databází, ale na úkor rychlosti. ORM musí nejen načíst data z databáze, ale také vytvořit instance objektů modelu a naplnit je daty ze sloupců.

Pokud máte 10 000 uživatelů a provedete `User::all()`, framework načte 10 000 řádků z databáze a provede 10 000x `new User()` a naplní jejich vlastnosti. Pokud je databáze na stejném místě jako aplikace, může to být úzké hrdlo, takže občas je lepší se ORM vyhnout.

To platí hlavně pro složité SQL dotazy. V takovém případě je lepší použít `DB::raw()` a napsat dotaz ručně.

Podle této studie i při jednoduchých vkládáních je Eloquent pomalejší s rostoucím počtem záznamů:

Používejte cache

Jedním z nejdůležitějších tajemství optimalizace je cache.

Cache je o tom, že si předpočítáte a uložíte nákladné výsledky (náročné na CPU a paměť), a pak je vrátíte, když se objeví stejný dotaz.

Například v e-shopu se lidé zajímají o produkty, které jsou čerstvě naskladněné, v určité cenové kategorii a pro určitou věkovou skupinu. Dotazování na to v databázi je zbytečné – protože se dotaz nemění, je lepší si výsledek uložit.

Laravel má vestavěnou podporu pro různé typy cache. Kromě použití ovladače pro cache můžete použít i balíčky pro cachování modelů, cachování dotazů atd.

Ale buďte si vědomi, že kromě jednoduchých případů mohou předem sestavené cache balíčky způsobit více problémů než užitku.

Preferujte cache v paměti

Když v Laravelu něco cachujete, máte na výběr, kam výsledek uložíte. Tomu se říká ovladače cache. Používat pro cache souborový systém je sice možné, ale není to ideální.

Ideální je používat paměť (RAM), jako je Redis, Memcached, MongoDB, atd. Pro velké zatížení, aby se cache sama nestala úzkým hrdlem.

Možná si myslíte, že SSD je skoro totéž co RAM, ale není. I neoficiální benchmarky ukazují, že RAM je 10-20x rychlejší než SSD.

Můj oblíbený systém pro cache je Redis. Je extrémně rychlý (běžně 100 000 operací za sekundu) a pro větší systémy se dá snadno škálovat do clusteru.

Cachujte routy

Stejně jako konfigurace, ani routy se moc nemění a jsou ideálním kandidátem pro cachování. To platí hlavně pokud máte velké soubory rout, nebo je rozdělíte do vícero souborů. Jeden příkaz Laravel sbalí všechny routy:

php artisan route:cache

A pokud přidáte nebo změníte routy, použijte:

php artisan route:clear

Optimalizujte obrázky a používejte CDN

Obrázky jsou důležitou součástí většiny webových aplikací. Ale jsou také velké a často zpomalují aplikace. Pokud nahrané obrázky jen uložíte na server a odesíláte je zpět, přijdete o možnost optimalizace.

Obrázky neukládejte lokálně – je zde riziko ztráty dat, a přenos dat může být pomalý. Použijte řešení jako Cloudinary, které automaticky mění velikost a optimalizuje obrázky.

Pokud to není možné, použijte něco jako Cloudflare pro cachování a zobrazování obrázků uložených na vašem serveru.

A pokud ani to není možné, aspoň dolaďte software webového serveru, aby komprimoval data a ukládal je do cache v prohlížeči. Zde je ukázka konfigurace pro Nginx:

server {

   # file truncated

    # gzip compression settings
    gzip on;
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_proxied any;
    gzip_vary on;

   # browser cache control
   location ~* .(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
         expires 1d;
         access_log off;
         add_header Pragma public;
         add_header Cache-Control "public, max-age=86400";
    }
}

Vím, že optimalizace obrázků s Laravelem nesouvisí, ale je to tak jednoduchý a mocný trik, že jsem ho nemohl vynechat.

Optimalizujte autoloader

Automatické načítání je skvělá funkce v PHP, která pravděpodobně zachránila jazyk od zkázy. Ale hledání a načítání třídy podle jmenného prostoru zabere čas. V produkčních nasazeních je žádoucí se tomu vyhnout. Laravel má i na to příkaz:

composer install --optimize-autoloader --no-dev

Používejte fronty

Fronty se používají, když je hodně úloh, které trvají několik milisekund. Například odesílání emailů – aplikace odesílá email upozornění, když uživatel provede akci.

Pokud u nového produktu chcete informovat management (6-7 emailových adres) při objednávce nad určitou hodnotu, a emailová brána odpovídá za 500ms, uživatel by čekal 3-4 sekundy. To není dobrá uživatelská zkušenost.

Řešením je uložit úlohy do fronty, informovat uživatele, že je vše v pořádku, a zpracovat úlohu později. Pokud dojde k chybě, lze úlohu opakovat.

Kredity: Microsoft.com

I když nastavení front vyžaduje trochu víc práce, je v moderní webové aplikaci nepostradatelné.

Optimalizujte assety (Laravel Mix)

Pro všechna front-end aktiva se ujistěte, že existuje kanál, který zkompiluje a minimalizuje všechny soubory. Pokud vám vyhovují systémy jako Webpack, Gulp, Parcel atd., nemusíte se o to starat, ale pokud to ještě neděláte, Laravel Mix je dobrá volba.

Mix je jednoduchá vrstva kolem Webpacku, která se stará o CSS, SASS, JS atd. pro produkci. Konfigurační soubor může být takto malý:

const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

Automaticky se postará o importy, minifikaci a optimalizaci. Mix zvládne nejen JS a CSS, ale i Vue a React komponenty.

Více informací zde!

Závěr

Optimalizace výkonu je spíš umění než věda – důležitější než *co* dělat, je *jak* a *kolik* toho udělat. Optimalizovat se toho dá hodně.

Ať už děláte cokoliv, optimalizujte, jen když to má smysl, a ne jen proto, že to zní dobře. Funkční aplikace, která dělá, co má, je lepší než aplikace, která je optimalizovaná až na kost, ale nefunguje pořádně.

Pokud si nejste jisti, zda potřebujete optimalizovat, raději se do toho nepouštějte. Spolehlivá aplikace je lepší než optimalizovaná a rozbitá. 🙂

A pokud se chcete stát mistrem v Laravelu, podívejte se na tento online kurz.

Ať vaše aplikace běží mnohem, mnohem rychleji! 🙂