Як оптимізувати веб-програму PHP Laravel для високої продуктивності?

Підвищення продуктивності Laravel: Поради та методи оптимізації

Laravel — це потужний інструмент, але його швидкість не завжди вражає. Розглянемо кілька стратегій, які допоможуть пришвидшити роботу ваших проєктів на Laravel.

Сьогодні, майже кожен PHP-розробник знайомий з Laravel. Початківці цінують швидкість розробки, а досвідчені спеціалісти вивчають його через попит на ринку.

Безсумнівно, Laravel дав новий імпульс PHP-екосистемі. Особисто я б давно покинув PHP без Laravel.

Зручність для розробника досягається за рахунок багатьох процесів, що виконуються “під капотом”. Кожна “магічна” функція Laravel — це насправді велика кількість коду, який налаштовується при кожному запуску. Навіть простий виняток проходить довгий шлях через різні рівні фреймворку, як видно з цього прикладу:

Уявіть собі, 18 викликів функцій, щоб відстежити помилку, що виникла у представленні. І це може бути навіть 40 і більше, якщо використовуються сторонні бібліотеки.

Тож, за замовчуванням, цей рівень абстракції робить Laravel повільним.

Наскільки повільний Laravel?

На це питання важко відповісти з кількох причин.

По-перше, немає загальноприйнятих стандартів для вимірювання швидкості веб-додатків. Порівняно з чим і за яких умов ми можемо визначити, що додаток є швидким або повільним?

По-друге, швидкість веб-додатка залежить від багатьох факторів (бази даних, файлова система, мережа, кеш). Швидкий додаток із повільною базою даних завжди буде працювати повільно. 🙂

Саме через цю невизначеність популярними є тести продуктивності. Хоча вони і не дають абсолютної відповіді (див. цю статтю і цю), вони створюють певну систему відліку.

Ось як виглядають результати порівняння швидкості PHP-фреймворків з цього GitHub репозиторію:

Laravel знаходиться в кінці списку. Звичайно, більшість цих фреймворків не є практичними, але це демонструє, наскільки повільним є Laravel порівняно з іншими популярними рішеннями.

Ця “повільність” часто не помітна в щоденних додатках, оскільки вони рідко досягають високих навантажень. Проте, при великій кількості користувачів (200-500 одночасних запитів), сервери починають перегріватись і виходити з ладу. У таких випадках навіть додаткове обладнання не вирішить проблеми, а витрати на інфраструктуру стрімко зростатимуть.

Але не хвилюйтеся! Ця стаття не про те, чого не можна робити, а про те, що можна. 🙂

Є багато способів зробити ваш додаток на Laravel швидшим. Навіть в кілька разів швидшим. І це не жарт. Ви можете суттєво збільшити продуктивність та заощадити кошти на інфраструктурі.

Чотири рівні оптимізації

Оптимізацію можна проводити на чотирьох різних рівнях (особливо, якщо мова йде про PHP-застосунки):

  • Рівень мови: використання швидшої версії PHP, уникнення певних конструкцій, які сповільнюють роботу.
  • Рівень фреймворку: саме ці методи ми розглянемо у цій статті.
  • Рівень інфраструктури: налаштування PHP-FPM, веб-сервера, бази даних.
  • Апаратний рівень: перехід до більш потужного хостинг-провайдера.

Всі ці рівні є важливими, але ми зосередимося на оптимізації на рівні фреймворку.

Примітка: ця нумерація рівнів оптимізації не є загальноприйнятою.

Отже, перейдемо до конкретних методів оптимізації.

Уникайте N+1 запитів до бази даних

Проблема N+1 запиту є типовою при роботі з ORM. У Laravel є потужний Eloquent ORM, який настільки зручний, що ми часто забуваємо про те, що відбувається під капотом.

Розглянемо типовий сценарій: відображення списку замовлень для кожного клієнта.

У Laravel це можна зробити так:

class OrdersController extends Controller 
{
    // ... 

    public function getAllByCustomers(Request $request, array $ids) {
        $customers = Customer::findMany($ids);        
        $orders = collect(); // new collection
        
        foreach ($customers as $customer) {
            $orders = $orders->merge($customer->orders);
        }
        
        return view('admin.reports.orders', ['orders' => $orders]);
    }
}
  

Ззовні виглядає добре, але це неефективний спосіб написання коду у Laravel.

Ось чому.

Коли ми запитуємо клієнтів, генерується SQL-запит:

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

Все логічно. Отримані рядки зберігаються в колекції `$customers`.

Далі ми перебираємо кожного клієнта і отримуємо його замовлення, що призводить до виконання запиту:

SELECT * FROM orders WHERE customer_id = 22;

Цей запит виконується стільки разів, скільки є клієнтів.

Тобто, для 1000 клієнтів буде виконано 1001 запит до бази даних (1 запит на отримання клієнтів і 1000 запитів на отримання замовлень). Звідси і назва – проблема N+1.

Як покращити ситуацію? Використати “жадібне завантаження” (eager loading). Це дозволить Eloquent виконати JOIN і повернути всі необхідні дані одним запитом:

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

Структура даних буде вкладеною, але ви легко зможете отримати потрібну інформацію. Відповідний SQL-запит виглядатиме приблизно так:

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

Один запит краще, ніж тисяча. Уявіть, що буде, якщо потрібно обробити 10000 клієнтів! “Жадібне завантаження” — це ваш друг.

Кешуйте конфігурацію

Гнучкість Laravel досягається завдяки великій кількості файлів конфігурації. Налаштування зберігання зображень? Файл `config/filesystems.php`. Кілька драйверів черги? `config/queue.php`. Всього в Laravel 13 файлів конфігурації.

При кожному веб-запиті Laravel завантажує і аналізує ці файли. Якщо нічого не змінилось, це непотрібні витрати. Щоб уникнути цього, скористайтеся командою:

php artisan config:cache

Вона об’єднує всі файли конфігурації в один кешований файл. Laravel буде використовувати цей файл для швидкого доступу до налаштувань.

Але будьте обережні! Після виконання цієї команди, виклики функції `env()` за межами файлів конфігурації будуть повертати `null`.

Це логічно. Коли використовуєте кешування конфігурації, ви кажете фреймворку: “Я впевнений, що все налаштував правильно і не хочу змін.” Тобто ви розраховуєте на статичне середовище, для якого призначені файли `.env`.

Правила кешування конфігурації:

  • Кешуйте конфігурацію тільки на продакшені.
  • Кешуйте тільки, якщо ви впевнені, що не потрібно змінювати конфігурацію.
  • Якщо щось пішло не так, очистіть кеш за допомогою `php artisan cache:clear`.
  • Сподівайтеся, що це не вплине на бізнес!

Зменшіть кількість автозавантажуваних провайдерів

Laravel завантажує багато сервіс-провайдерів при запуску. Вони визначені у файлі `config/app.php` в масиві `’providers’`. Ось приклад:

/*
    |--------------------------------------------------------------------------
    | 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...
         */
        IlluminateAuthAuthServiceProvider::class,
        IlluminateBroadcastingBroadcastServiceProvider::class,
        IlluminateBusBusServiceProvider::class,
        IlluminateCacheCacheServiceProvider::class,
        IlluminateFoundationProvidersConsoleSupportServiceProvider::class,
        IlluminateCookieCookieServiceProvider::class,
        IlluminateDatabaseDatabaseServiceProvider::class,
        IlluminateEncryptionEncryptionServiceProvider::class,
        IlluminateFilesystemFilesystemServiceProvider::class,
        IlluminateFoundationProvidersFoundationServiceProvider::class,
        IlluminateHashingHashServiceProvider::class,
        IlluminateMailMailServiceProvider::class,
        IlluminateNotificationsNotificationServiceProvider::class,
        IlluminatePaginationPaginationServiceProvider::class,
        IlluminatePipelinePipelineServiceProvider::class,
        IlluminateQueueQueueServiceProvider::class,
        IlluminateRedisRedisServiceProvider::class,
        IlluminateAuthPasswordsPasswordResetServiceProvider::class,
        IlluminateSessionSessionServiceProvider::class,
        IlluminateTranslationTranslationServiceProvider::class,
        IlluminateValidationValidationServiceProvider::class,
        IlluminateViewViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        AppProvidersAppServiceProvider::class,
        AppProvidersAuthServiceProvider::class,
        // AppProvidersBroadcastServiceProvider::class,
        AppProvidersEventServiceProvider::class,
        AppProvidersRouteServiceProvider::class,

    ],
  

В цьому прикладі 27 провайдерів. Можливо, вам всі вони не потрібні.

Наприклад, якщо ви створюєте REST API, вам не потрібен провайдер сесій або представлень. Якщо ви не використовуєте стандартні налаштування фреймворку, можна вимкнути провайдери автентифікації, розбиття на сторінки, перекладів і т.д. У багатьох випадках половина з них непотрібна.

Перевірте свій додаток. Чи потрібні вам всі ці провайдери? Не вимикайте їх сліпо і не випускайте на продакшен! Перевіряйте зміни локально і на тестовому середовищі.

Будьте уважні зі стеком проміжного ПЗ

Проміжне програмне забезпечення дозволяє обробляти вхідні веб-запити. Не спокушайтесь додавати все проміжне ПЗ до стеку веб або API в `app/Http/Kernel.php`. Це може стати тягарем, якщо все проміжне програмне забезпечення виконується для кожного запиту.

Будьте обережні з тим, куди ви додаєте нове проміжне ПЗ. Додавати його глобально зручно, але це може негативно вплинути на продуктивність у довгостроковій перспективі.

Уникайте ORM (іноді)

Eloquent спрощує роботу з БД, але це впливає на швидкість. ORM не лише отримує дані з БД, але й створює екземпляри моделей і наповнює їх даними.

Якщо ви виконуєте `$users = User::all()` і є 10000 користувачів, фреймворк отримає 10000 рядків і створить 10000 об’єктів `User()`, заповнивши їх відповідними даними. Це велика кількість роботи. Якщо база даних стає вузьким місцем, іноді варто обійти ORM.

Це особливо актуально для складних SQL-запитів. У таких випадках використовуйте `DB::raw()` і напишіть запит вручну.

Згідно з цим дослідженням, навіть для простих вставок Eloquent працює значно повільніше зі збільшенням кількості записів:

Використовуйте кешування

Кешування — це один із найефективніших способів оптимізації веб-додатків.

Кешування означає попереднє обчислення і збереження дорогих результатів (з точки зору використання процесора і пам’яті) для повторного використання.

Наприклад, для інтернет-магазину з мільйонами товарів, більшість користувачів шукають товари в певній категорії і ціновій категорії. Немає сенсу постійно запитувати базу даних. Краще зберегти ці результати в кеш.

Laravel має вбудовану підтримку кешування. Також є сторонні пакети для кешування моделей, кешування запитів та інші.

Але пам’ятайте, що сторонні пакети не завжди є оптимальним рішенням.

Віддавайте перевагу кешуванню в пам’яті

Laravel дозволяє зберігати кеш у різних місцях, таких як файлова система або пам’ять. Ці варіанти називаються драйверами кешу. Зберігання кешу у файловій системі є прийнятним, але це не найефективніший варіант.

Бажано використовувати кеш у пам’яті (RAM), наприклад, Redis, Memcached, MongoDB. Це гарантує, що кеш не стане вузьким місцем при високих навантаженнях.

SSD-диски швидші за HDD, але RAM все одно набагато швидший. Неформальні тести показують, що RAM перевершує SSD в 10-20 разів.

Redis — чудова система для кешування. Він дуже швидкий (100 000 операцій читання в секунду — це норма), і його можна легко перетворити на кластер для великих систем.

Кешуйте маршрути

Подібно до конфігурації, маршрути не часто змінюються, тому їх можна кешувати. Це особливо актуально, якщо ви розбиваєте файли `web.php` і `api.php` на декілька файлів. Команда Laravel збирає всі доступні маршрути і зберігає їх для майбутнього використання:

php artisan route:cache

При додаванні або зміні маршрутів, очистіть кеш командою:

php artisan route:clear

Оптимізація зображень та CDN

Зображення є важливою частиною більшості веб-додатків, але вони також споживають найбільше пропускної здатності. Якщо ви просто зберігаєте зображення на сервері, то втрачаєте можливості оптимізації.

Не зберігайте зображення локально. Це призводить до втрати даних, а швидкість передачі даних залежить від географічного розташування користувача.

Використовуйте рішення, як Cloudinary, яке автоматично змінює розмір та оптимізує зображення. Якщо це неможливо, використовуйте Cloudflare для кешування зображень, поки вони зберігаються на вашому сервері.

Налаштуйте веб-сервер для стиснення ресурсів та кешування. Ось приклад конфігурації 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";
    }
}
  

Оптимізація зображень не пов’язана безпосередньо з Laravel, але це важливий крок для підвищення продуктивності.

Оптимізація автозавантажувача

Автозавантаження — це функція PHP, яка дозволяє знаходити і завантажувати класи. Але цей процес займає час, тому його потрібно оптимізувати для виробничих розгортань. Laravel має просте рішення:

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

Використовуйте черги

Черги використовуються для обробки задач, які займають певний час. Наприклад, відправка електронних листів. Замість того, щоб затримувати відповідь користувачу, зберігайте завдання в чергу і обробляйте їх пізніше.

Наприклад, коли користувач робить замовлення, можна відправляти сповіщення електронною поштою. Якщо відправка листа займає 500 мс, а потрібно відправити 7 сповіщень, користувачу доведеться чекати 3-4 секунди. Черги дозволяють обробляти ці завдання асинхронно, не блокуючи користувача.

Авторство: Microsoft.com

Черги вимагають певного налаштування, але є незамінними для сучасних веб-додатків.

Оптимізація активів (Laravel Mix)

Використовуйте конвеєр для компіляції та мінімізації всіх файлів ресурсів. Якщо ви не використовуєте такі інструменти, як Webpack, Gulp, Parcel, то Laravel Mix буде гарним варіантом.

Mix — це оболонка Webpack, яка мінімізує CSS, SASS, JS файли. Звичайний файл `mix.js` може виглядати так:

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

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

Mix подбає про імпорт, мініфікацію, оптимізацію, коли ви запустите `npm run production`. Mix працює не лише з JS і CSS, але і з Vue, React.

Більше інформації тут!

Висновок

Оптимізація продуктивності — це мистецтво, а не наука. Важливо знати, як і коли потрібно оптимізувати. Немає меж тому, що можна оптимізувати у додатку Laravel.

Оптимізуйте, якщо є вагома причина, а не просто тому, що це звучить добре. Програма, яка працює і виконує свої функції, краще, ніж оптимізована, але нестабільна.

Якщо ви не впевнені, чи потрібно вам оптимізувати додаток, не потрібно цього робити. Працюючий додаток, який виконує свою роботу, краще за оптимізований, але нестабільний.

Для тих, хто хоче глибше вивчити Laravel, є онлайн-курс.

Нехай ваші додатки працюють швидко! 🙂