<? phpukraine СПІВБЕСІДИ
Пошук по платформі
LARAVEL · SENIOR

Livewire чи API з SPA: як обираєте архітектуру фронтенду для Laravel-проєкту?

Вибір визначають три речі, а не смак: чи є другий клієнт (мобілка, партнери) — тоді API все одно доведеться зробити, і SPA стає майже безкоштовним; яка зернистість взаємодій — Livewire платить мережевим round-trip за кожен крок, тому годиться для форм, таблиць і фільтрів, а не для перетягування, канви чи офлайну; і хто це супроводжуватиме. Livewire тримає стан на сервері й ганяє снапшот компонента туди-сюди, SPA тримає стан у браузері й вимагає версіонованого контракту; Inertia — середина, де є клієнтські компоненти, але немає публічного API.

Адмінка на 60 CRUD-екранів, команда — три бекендери й пів фронтендера. Livewire чи Vue з REST?
Кажуть, Livewire повільний, бо кожен клік іде на сервер. Це так, і в чому саме ціна?
Що конкретно летить у браузер, коли в компонента є `public User $user`, і чи можу я це підмінити з devtools?
Livewire чи Inertia — якщо і там, і там немає окремого API, у чому тоді різниця?
У нас уже є мобільний застосунок. Це щось змінює у виборі?
Livewire SPA Inertia архітектура фронтенд API

Спершу механіка, бо без неї суперечка зводиться до смаків. Livewire — це серверні компоненти зі станом, який подорожує разом із клієнтом. При першому рендері в HTML вкладається снапшот: JSON з усіма публічними властивостями, іменем класу, ідентифікатором компонента й memo-даними, підписаний HMAC-чексумою від APP_KEY. Кожна взаємодія — це POST на /livewire/update з парою snapshot плюс calls/updates; кілька компонентів на сторінці за замовчуванням батчаться в один запит. Сервер не памʼятає ваш обʼєкт — він створює новий і гідратує його зі снапшота (властивості-моделі зберігаються як клас плюс первинний ключ і перезапитуються з БД), виконує метод, повністю перерендерює Blade і повертає HTML, який клієнтський морфер накладає на наявний DOM. Звідси випливає все інше: застосунок лишається stateless і масштабується звичайними інстансами, але ціна кожної взаємодії — RTT плюс бутстрап фреймворку плюс рендер, а розмір публічних властивостей — це трафік, помножений на кількість кліків.

Протилежний варіант — API плюс SPA — переносить стан у браузер. Сервер віддає JSON через контролери й JsonResource, клієнт має власний роутер, власний кеш і власну модель даних; взаємодії всередині екрана нічого не коштують серверу, зате зʼявляється другий контракт, який треба версіонувати, документувати, тестувати й окремо авторизувати. Автентифікація роздвоюється: laravel/sanctum у SPA-режимі на тому самому домені працює на сесійних куках із CSRF, а мобільний клієнт ходить з токенами. Третій варіант — Inertia — навмисно не є API: контролер повертає Inertia::render('Orders/Index', $props), роутинг і авторизація лишаються на сервері, а Vue чи React рендерять сторінку з props як зі звичайного JSON. Це дає справжні клієнтські компоненти без публічного контракту, але вимагає фронтенд-стека й окремого SSR-процесу (php artisan inertia:start-ssr), якщо потрібен індексований HTML.

Тепер критерії, за якими насправді ухвалюють рішення. Перший і найважливіший: чи існує другий клієнт. Якщо є мобільний застосунок, партнерські інтеграції або публічний API — контракт у кошторисі вже стоїть, і SPA поверх нього коштує додатково небагато; будувати ж API «на майбутнє», якого немає в роадмапі, — це подвоєння роботи без споживача. Другий: зернистість взаємодій. Форми, таблиці, фільтри, майстри, адмінки — усе, де користувач робить дію й чекає результату, — чудово живуть на round-trip. Перетягування, канва, редактор, зум графіка, офлайн і оптимістичні оновлення — не живуть: там кожен рух миші перетворився б на HTTP-запит. Третій: команда й чергування. Три бекендери без фронтенд-компетенції на SPA платитимуть щодня — збіркою, дублюванням типів, розсинхроном контракту; для них Livewire з wire:navigate дає SPA-подібну навігацію без другого стека.

Практика зводиться до кількох дисциплін, які й відрізняють senior-відповідь. У Livewire публічні властивості — це клієнтський контракт: їх видно в devtools і їх можна виставляти через updates, тому ідентифікатори власника позначають #[Locked], секрети туди не кладуть узагалі, а права перевіряють у самій дії (або через #[Authorize]), бо mount() виконується лише при першому рендері. Похідні дані — це #[Computed], а не public: результат кешується в межах запиту й не роздуває снапшот. Кількість запитів зменшують wire:model без .live там, де вистачає синхронізації з сабмітом, .debounce.300ms там, де потрібен живий пошук, #[Renderless] для дій без змін у розмітці, #[Lazy] для важких блоків і острівці Livewire 4 (@island), що перерендерюють частину компонента замість усього дерева. У @foreach обовʼязковий wire:key з реальним ідентифікатором, інакше після сортування морфер зіставить не ті вузли. У SPA-варіанті дисципліни інші: білий список полів у ресурсах, стабільна пагінація, версія в шляху й тести, які ловлять зміну контракту, — деталі цього розібрані в картці про JsonResource.

Межі й компроміси варто назвати вголос, бо на них перевіряють зрілість. Livewire не усуває JavaScript — на сторінці є його рантайм і Alpine, і будь-який складний віджет усе одно доведеться писати руками, загорнувши сторонню бібліотеку в wire:ignore. Livewire множить кількість PHP-запитів на кількість кліків, тож планувати потужність треба від взаємодій, а не від сторінок; персоналізовані POST-відповіді на CDN не кешуються, а Octane прибирає бутстрап, але вимагає чистих статичних станів і синглтонів. SPA, у свою чергу, платить за SEO рендером на сервері, за автентифікацію — двома різними схемами, а за кожну зміну поля — координацією двох релізів. І головне: вибір не бінарний і не остаточний. Типова здорова архітектура — Livewire на CRUD і адмінці, два-три ізольовані клієнтські віджети на Alpine або на власних компонентах із синхронізацією через $wire.entangle, і API рівно для тих споживачів, які справді існують. Якщо на співбесіді ви покажете саме цей шлях міграції — від сторінки до острівця, а не від фреймворку до фреймворку, — питання закрите.

use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Livewire\Attributes\{Computed, Locked, Url};
use Livewire\{Component, WithPagination};

final class OrderList extends Component
{
    use WithPagination;

    // Публічна властивість = частина снапшота: їде в браузер і назад на кожну дію.
    // Без #[Locked] клієнт легально виставляє її через updates у payload —
    // чексума від цього не рятує, вона захищає лише те, що надіслав сервер.
    #[Locked]
    public int $merchantId;

    #[Url(as: 'q', history: true)]
    public string $search = '';

    public function mount(int $merchantId): void
    {
        // Виконується лише при першому рендері: наступні запити гідратують
        // снапшот і одразу йдуть у дію, тому авторизація тут нічого не захищає.
        $this->merchantId = $merchantId;
    }

    public function updatedSearch(): void
    {
        $this->resetPage(); // інакше новий фільтр застосується до сторінки 7
    }

    public function markShipped(int $orderId): void
    {
        $order = Order::where('merchant_id', $this->merchantId)->findOrFail($orderId);
        $this->authorize('ship', $order); // права перевіряємо в дії, а не в mount()
        $order->markShipped();
    }

    // Не властивість: не серіалізується у снапшот і не їде клієнту.
    #[Computed]
    public function orders(): LengthAwarePaginator
    {
        return Order::query()
            ->where('merchant_id', $this->merchantId)
            ->when($this->search !== '', fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
            ->with('customer') // рендер Blade так само породжує N+1
            ->paginate(20);
    }
}

// Blade: <input wire:model.live.debounce.300ms="search">
// .live обовʼязково: з Livewire 3 біндинг відкладений і синхронізується з дією.
Що рішення виводиться з вимог (другий клієнт, зернистість взаємодій, склад команди, вимоги до офлайну й SEO), а не з тези «Livewire простіший».
Розуміння протоколу Livewire: снапшот із публічних властивостей їде в браузер і назад на кожну дію, підписаний HMAC від `APP_KEY`, сервер перерендерює Blade і повертає HTML, який морфиться в DOM.
Що публічні властивості — це клієнтський контракт: їх видно й можна змінювати з payload, тому `#[Locked]`, жодних секретів і авторизація в дії, а не в `mount()`.
Що Inertia — це третій, а не проміжний варіант: серверний роутинг і контролери, клієнтські компоненти, props як JSON і жодного публічного API для підтримки.
Розрахунок навантаження: у Livewire кожна взаємодія — це PHP-запит із бутстрапом фреймворку, тоді як у SPA більшість взаємодій не доходить до сервера; для авторизованих сторінок CDN не рятує ні там, ні там.
Що гібрид — нормальний результат: Livewire на CRUD, Alpine/JS-острівці на важкі віджети, API — лише там, де є справжній споживач.
Продавати Livewire як «фронтенд без JavaScript»: на сторінці все одно є рантайм Livewire і Alpine, а `wire:key` у циклах треба ставити руками, інакше морфінг переплутає рядки після сортування.
Класти в публічну властивість колекцію на тисячі рядків або результат запиту: він серіалізується у снапшот і їде по мережі двічі на кожен клік. Похідні дані — це `#[Computed]`, а не `public`.
Вважати, що чексума снапшота захищає значення: вона захищає від підміни того, що надіслав сервер, але `updates` у payload легально виставляє будь-яку публічну властивість — без `#[Locked]` `merchantId` переписується з консолі.
Перевіряти права в `mount()`: він виконується лише при першому рендері, наступні запити гідратують снапшот і одразу викликають дію — авторизація має бути в самій дії або в `#[Authorize]`.
Будувати повноцінний REST/GraphQL «на випадок майбутньої мобілки», якої немає в роадмапі: це другий контракт, другий набір тестів і другий шар авторизації без жодного споживача.
Обирати SPA заради SEO, а потім місяць прикручувати SSR: Livewire і Inertia з SSR віддають готовий HTML, чистий CSR — ні.
Дивуватися, що `wire:model="search"` не оновлює список: з Livewire 3 біндинг за замовчуванням відкладений, потрібен `wire:model.live` (і `.debounce`, щоб не слати запит на кожну літеру).
Ставити Livewire на високочастотні взаємодії — drag-and-drop, редактор, графік із зумом: кожен рух миші перетворюється на HTTP-запит замість локального стану.
ПОРАДА

Не відповідайте «залежить». Назвіть три критерії вголос і пройдіться по них: (1) чи існує другий клієнт — якщо так, API вже в кошторисі, і SPA дешевшає; (2) чи є взаємодії частіші за клік — якщо так, вони не переживуть round-trip; (3) хто чергує вночі — команда без фронтенд-компетенції платитиме за SPA щодня. І одразу додайте, що вибір не бінарний: Livewire на CRUD плюс два ізольовані JS-віджети — типова, а не компромісна архітектура.

оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Стан компонента живе не в сесії, а в снапшоті, що подорожує з клієнтом, — саме тому Livewire масштабується як звичайний stateless-застосунок. Чексума HMAC гарантує лише цілісність того, що надіслав сервер; секція `updates` у запиті призначена для того, щоб клієнт задавав публічні властивості, тож `#[Locked]` — це справжній захист, а не стиль. Inertia навмисно не є API: props прив'язані до конкретної сторінки й компонента, версіонованого контракту немає, і мобільний застосунок їх не спожив би.