Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Продуктивність Livewire

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

12 питань

Після кожного запиту Livewire не замінює HTML компонента повністю, а морфить DOM: порівнює старе й нове дерево елемент за елементом і змінює лише відмінності. Так зберігаються фокус, введений текст, стан Alpine.

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

@foreach ($todos as $todo)
    <li>
        <input type="checkbox" wire:model="done.{{ $todo->id }}">
        <span x-data="{ editing: false }">{{ $todo->title }}</span>
    </li>
@endforeach

Наслідки: стан Alpine (editing), фокус і введення «переїжджають» на сусідні рядки, анімації спрацьовують не там, а вкладені Livewire-компоненти отримують чужі дані.

wire:key дає кожному елементу стабільний ідентифікатор - морфінг зіставляє елементи за ключем:

@foreach ($todos as $todo)
    <li wire:key="todo-{{ $todo->id }}">...</li>
@endforeach

@foreach ($posts as $post)
    <livewire:post-card :$post :wire:key="$post->id" />
@endforeach

Правила:

  • ключ - ідентифікатор даних (id), а не індекс циклу. $loop->index не допомагає: після видалення елемента індекси зсуваються так само;
  • ключ має бути унікальним у межах компонента. Якщо на сторінці два списки з тих самих моделей, додайте префікс: "comment-{{ $id }}", "reply-{{ $id }}";
  • для вкладених Livewire-компонентів у циклі ключ обов'язковий - без нього стан дочірніх компонентів змішується.

Налаштування smart_wire_keys (у Livewire 4 увімкнено за замовчуванням) допомагає з ключами глибоко вкладених компонентів, але не звільняє від wire:key у циклах.

Ще один випадок - залежні поля: <select> міст, що змінюється разом з обраною областю, потребує wire:key="{{ $regionId }}", щоб значення коректно скинулося.

Альтернатива морфінгу для складних випадків - wire:replace: замінити вміст елемента повністю замість порівняння.

Докладніше в документації: Морфінг

wire:poll періодично надсилає запит до компонента й оновлює його - найпростіший спосіб показувати свіжі дані без WebSocket.

<div wire:poll>
    Нових замовлень: {{ $this->newOrdersCount }}
</div>

<div wire:poll.15s="refreshStatus">
    Статус імпорту: {{ $status }}
</div>

За замовчуванням - кожні 2,5 секунди з повторним рендером компонента; з назвою методу - викликається цей метод.

Чому це дорого: тисяча відкритих вкладок з інтервалом 2,5 с - 400 запитів на секунду на сервер, кожен з гідрацією компонента, запитами до бази й рендером. Для PHP-FPM це сотні зайнятих процесів лише на опитування.

Як зменшити навантаження:

  • довший інтервал. Найефективніший і найпростіший крок: .10s, .1m замість типових 2,5 с;
  • .visible - опитувати лише тоді, коли елемент видно на екрані. Блок унизу довгої сторінки не опитується, поки до нього не прокрутять;
  • фонові вкладки Livewire пригальмовує автоматично - на 95%. Модифікатор .keep-alive вимикає це, і без справжньої потреби його не варто ставити;
  • легкий метод опитування: перевірити дешеву ознаку змін (кількість, max(updated_at)) і не перебудовувати важкі дані, якщо нічого не змінилося;
  • острівець (@island) з опитуванням - оновлюється лише ця частина компонента, а не весь шаблон;
  • #[Isolate] - щоб повільне опитування одного компонента не затримувало запити інших.

Livewire 4: опитування більше не блокує інші дії - клік користувача не чекає на запит опитування в черзі.

Коли опитування - неправильний інструмент:

  • оновлення потрібні миттєво (чат, спільне редагування) - WebSocket (Laravel Reverb + Echo), і Livewire слухає події трансляції через #[On('echo:...')];
  • оновлення рідкісні, а відвідувачів багато - теж краще події, ніж тисячі порожніх запитів;
  • зміна станеться один раз (завершення імпорту) - опитування з зупинкою: перестаньте рендерити wire:poll, коли статус фінальний.

Докладніше в документації: wire:poll

Компонент з повільним запитом (статистика, звіт, зовнішній API) затримує всю сторінку: сервер не відправить HTML, поки не виконає mount() кожного компонента.

Два способи відкласти:

<livewire:revenue-chart lazy />    {{-- коли стане видимим на екрані --}}
<livewire:revenue-chart defer />   {{-- одразу після завантаження сторінки --}}
  • lazy - компонент завантажується, коли потрапляє в область видимості (прокрутили до нього). Для блоків унизу сторінки, які можуть і не знадобитися;
  • defer - завантажується одразу після першого показу сторінки, незалежно від прокрутки. Для того, що потрібно завжди, але не повинно затримувати першу відповідь.

Сторінка приходить швидко з заглушкою на місці компонента, а сам компонент підвантажується окремим запитом.

Заглушка (placeholder) - що бачить користувач, поки компонент вантажиться:

@placeholder
    <div class="h-64 animate-pulse rounded bg-gray-100"></div>
@endplaceholder

<div>
    Дохід цього місяця: {{ $amount }}
</div>

У класових компонентах - метод placeholder(). Кореневий тег заглушки й компонента має збігатися (<div> і <div>). Добра заглушка має той самий розмір, що й готовий блок, - інакше сторінка «стрибатиме».

Зробити відкладеним завжди - атрибути на класі #[Lazy] чи #[Defer]. Для цілої сторінки - Route::livewire(...)->lazy().

Об'єднання запитів. За замовчуванням кожен відкладений компонент вантажиться окремим паралельним запитом. Десять віджетів на дашборді - десять запитів. Можна об'єднати в один:

<livewire:revenue lazy.bundle />

Але тоді повільний віджет затримає швидкі - об'єднання доречне для схожих за швидкістю компонентів.

Що варто знати:

  • параметри, передані відкладеному компоненту (:user="$user"), серіалізуються й чекають у браузері до завантаження - тож моделі в них перезавантажуються з бази;
  • SEO: вміст відкладеного компонента відсутній у початковому HTML;
  • у тестах - Livewire::withoutLazyLoading(), щоб бачити справжній вміст, а не заглушку;
  • для частини компонента, а не цілого компонента, - острівець @island(lazy: true).

Докладніше в документації: Ліниве завантаження

Обчислювана властивість - метод з атрибутом #[Computed], до якого звертаються як до властивості. Результат кешується в межах одного запиту.

use Livewire\Attributes\Computed;

new class extends Component {
    public string $search = '';

    #[Computed]
    public function users()
    {
        return User::query()
            ->where('name', 'like', "%{$this->search}%")
            ->with('team')
            ->limit(20)
            ->get();
    }
};
@foreach ($this->users as $user)
    <li wire:key="{{ $user->id }}">{{ $user->name }} ({{ $user->team->name }})</li>
@endforeach
<p>Знайдено: {{ $this->users->count() }}</p>   {{-- повторний доступ - без нового запиту --}}

У шаблоні - лише через $this->: $this->users, а не $users.

Чим краще за публічну властивість (public $users заповнена в mount()):

  • не потрапляє в знімок стану - не їде в браузер і назад з кожним запитом;
  • не перезавантажується з бази на кожен запит гідрації - лише коли шаблон чи код справді звертається до неї;
  • завантажені зв'язки (with('team')) працюють, бо запит виконується повністю щоразу, а не відновлюється за ключами;
  • завжди актуальна - похідна від поточного стану ($search), її не треба оновлювати вручну.

Ліниве виконання - головна перевага: якщо дані потрібні лише в гілці @if, запит виконається лише тоді, коли умова істинна. А в острівці - лише коли рендериться саме острівець.

Скинути кеш у межах запиту, якщо дані змінилися:

public function addUser(): void
{
    User::create([...]);
    unset($this->users);   // наступне звернення виконає запит заново
}

Кешування між запитами - #[Computed(persist: true)] (на компонент, година за замовчуванням) чи #[Computed(cache: true)] (спільно для всіх екземплярів) - але з обережністю щодо персональних даних.

Правило: публічні властивості - для стану, який змінює користувач (фільтри, введені значення, обраний id). Дані, що з них виводяться, - обчислювані властивості.

Докладніше в документації: Обчислювані властивості

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

<div>
    <h1>Дашборд</h1>

    @island
        <div>
            Дохід: {{ $this->revenue }}
            <button type="button" wire:click="$refresh">Оновити</button>
        </div>
    @endisland

    <livewire:recent-orders />
</div>

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

Можливості:

  • ліниве завантаження частини компонента: @island(lazy: true) чи @island(defer: true) з @placeholder усередині;
  • іменовані острівці і виклик з будь-якого місця компонента:
@island(name: 'stats') ... @endisland
<button wire:click="$refresh" wire:island="stats">Оновити статистику</button>
  • дописування замість заміни - для «Завантажити ще» й нескінченної стрічки: wire:island.append="feed" чи .prepend;
  • always: true - острівець оновлюється разом з кожним рендером компонента;
  • skip: true - не рендерити, доки не викличуть явно.

Важлива поведінка за замовчуванням: коли рендериться весь компонент, острівці пропускаються. Змінили фільтр поза острівцем - острівець, що від нього залежить, сам не оновиться (потрібен always: true або явний виклик за назвою).

Чим краще за дочірній компонент:

  • немає окремого файлу, стану, параметрів і подій для синхронізації - острівець бачить властивості й методи компонента;
  • не потрібен #[Reactive] для передачі змін.

Коли все ж дочірній компонент:

  • повторне використання в кількох місцях;
  • власні хуки життєвого циклу, власна авторизація в mount();
  • складний ізольований стан.

Обмеження:

  • острівці не можна ставити в @foreach, @if та інші керуючі конструкції - вони не мають доступу до змінних циклу чи умови;
  • запити острівців ідуть паралельно і змінюють той самий стан компонента: якщо кілька запитів у польоті одночасно, «перемагає» відповідь, що прийшла останньою. Дії, що змінюють спільні властивості, варто тримати поза паралельними острівцями.

Докладніше в документації: Острівці

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

#[Renderless] - дія без рендеру:

use Livewire\Attributes\Renderless;

new class extends Component {
    public Post $post;

    #[Renderless]
    public function trackRead(): void
    {
        $this->post->increment('reads');
    }
};
<div wire:intersect.once="trackRead">...</div>

Інші способи:

  • $this->skipRender() усередині методу - коли рендер треба пропустити за умовою:
public function saveDraft(): void
{
    $this->post->update(['draft' => $this->draft]);

    if (! $this->showSavedAt) {
        $this->skipRender();   // показувати нічого не треба - рендер зайвий
    }
}
  • модифікатор .renderless у шаблоні - точково, без зміни класу: wire:click.renderless="trackClick", wire:model.live.renderless="draft".

Типові застосування:

  • аналітика й лічильники (перегляди, кліки, час на сторінці);
  • збереження чернетки чи налаштувань, які вже відображені в браузері (стан у полі чи в Alpine);
  • дії, результат яких обробляє JavaScript (повернене значення з await $wire.method()).

Пастки:

  • зміни властивостей у такій дії не з'являться на екрані - шаблон не рендерився. Якщо дія змінює те, що показується, рендер потрібен;
  • помилки валідації в дії без рендеру теж не відобразяться через @error - для таких випадків потрібен рендер чи обробка в JavaScript;
  • стан компонента все одно оновлюється - новий знімок повертається в браузер, тож наступні дії бачитимуть змінені значення.

Поєднання з #[Async] - класична пара для «запустив і забув»: дія не стає в чергу за іншими й не перемальовує компонент.

Тести: ->call('trackRead')->assertRenderSkipped().

Докладніше в документації: Атрибут Renderless

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

Групування між компонентами (bundling). Якщо кілька компонентів сторінки оновлюються одночасно (подія, на яку реагують три компоненти, кілька опитувань), Livewire об'єднує їхні оновлення в один HTTP-запит. Менше з'єднань, менше навантаження, а також це основа для механізмів, що потребують узгодження компонентів (реактивні параметри, #[Modelable]).

Мінус групування: спільний запит відповідає тоді, коли закінчив найповільніший компонент. Повільний віджет з викликом зовнішнього API затримує оновлення решти сторінки.

#[Isolate] - запити компонента не групуються з іншими і йдуть паралельно:

use Livewire\Attributes\Isolate;

new #[Isolate] class extends Component {
    public function refreshShipping(): void
    {
        $this->status = Carrier::trackParcel($this->trackingNumber);   // 2-3 секунди
    }
};

Коли ізолювати:

  • дорогі операції (складні запити, зовнішні API, важкі обчислення);
  • кілька компонентів з wire:poll з різними інтервалами;
  • компоненти, що слухають ті самі події, де один повільний не повинен гальмувати інших;
  • компонент не взаємодіє з іншими через реактивні параметри.

Ціна ізоляції - більше паралельних запитів і процесів PHP. Ізолювати все підряд - отримати десятки запитів замість одного.

Пов'язані механізми:

  • ліниві компоненти ізольовані за замовчуванням - кожен вантажиться окремим паралельним запитом; lazy.bundle / #[Lazy(bundle: true)] об'єднує їх;
  • #[Async] / .async - не стосується групування між компонентами, а виводить дію з черги свого компонента: вона виконується одразу, паралельно з іншими діями;
  • Livewire 4 зробив опитування й wire:model.live неблокувальними: швидке введення в полі пошуку не чекає завершення попереднього запиту.

Діагностика: вкладка Network у DevTools - видно, скільки запитів livewire/update іде, які компоненти в кожному (у тілі запиту - масив компонентів) і скільки триває кожен.

Докладніше в документації: Атрибут Isolate

Звичайна пагінація Laravel перезавантажує сторінку на кожен перехід. У Livewire-компоненті перехід між сторінками відбувається без перезавантаження, а номер сторінки все одно записується в URL (?page=2).

use Livewire\Attributes\Computed;
use Livewire\WithPagination;

new class extends Component {
    use WithPagination;

    public string $search = '';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    #[Computed]
    public function posts()
    {
        return Post::query()
            ->where('title', 'like', "%{$this->search}%")
            ->with('author')
            ->latest()
            ->paginate(20);
    }
};
<input wire:model.live.debounce.300ms="search">

@foreach ($this->posts as $post)
    <article wire:key="{{ $post->id }}">...</article>
@endforeach

{{ $this->posts->links() }}

Ключові моменти:

  • трейт WithPagination обов'язковий - без нього посилання пагінації ведуть на звичайні URL з перезавантаженням;
  • пагінатор - в обчислюваній властивості чи в render(), а не в публічній властивості: пагінатор не серіалізується як стан компонента;
  • resetPage() при зміні фільтра - інакше користувач на 5-й сторінці, змінивши пошук, побачить порожню 5-ту сторінку нових результатів;
  • методи навігації: setPage(3), nextPage(), previousPage().

Кілька пагінаторів на сторінці конфліктують за параметр page - потрібна власна назва:

return Invoice::paginate(10, pageName: 'invoices-page');
$this->resetPage(pageName: 'invoices-page');

Швидкість на великих таблицях:

  • paginate() виконує ще й COUNT(*) по всьому набору - на мільйонах рядків це дорого;
  • simplePaginate() - лише «Попередня / Наступна», без підрахунку;
  • cursorPaginate() - курсор у URL замість номера сторінки, не сповільнюється на глибоких сторінках (немає OFFSET), але не дає перейти на довільну сторінку.

Прокрутка після переходу. Посилання пагінації Livewire після кожного переходу прокручують до початку сторінки. Якщо список - не перший блок на сторінці, краще прокручувати до нього: {{ $this->posts->links(data: ['scrollTo' => '#posts']) }}, або вимкнути прокрутку ('scrollTo' => false).

Без номера сторінки в URL - трейт WithoutUrlPagination поряд з WithPagination (наприклад, для списку в модальному вікні).

Нескінченна стрічка замість сторінок - острівець з wire:island.append і wire:intersect на «маркері» внизу списку.

Докладніше в документації: Пагінація

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

use Livewire\Attributes\Async;

new class extends Component {
    #[Async]
    public function trackClick(string $url): void
    {
        Analytics::track('external-link', ['url' => $url, 'user' => auth()->id()]);
    }
};
<a href="{{ $url }}" target="_blank" wire:click.async="trackClick('{{ $url }}')">Перейти</a>

Або точково в шаблоні: wire:click.async, wire:intersect.async.

Чому стан і асинхронність несумісні. Кожен запит стартує зі знімка стану, який браузер мав на момент відправки. Паралельні запити не бачать результатів одне одного:

#[Async]   // так не можна
public function increment(): void
{
    $this->count++;
}

П'ять швидких кліків - п'ять запитів, кожен починає з count = 0, кожен повертає 1. Лічильник показує 1 замість 5. Зміни губляться, а остаточний стан визначає відповідь, що прийшла останньою, - не обов'язково остання за часом дія.

Безпечні сценарії:

  • чисті побічні ефекти: аналітика, журнали, постановка джоби в чергу, сповіщення - дія не змінює властивостей, що відображаються;
  • дані для JavaScript: результат повертається в Alpine і зберігається там, а не в стані компонента:
<div x-data="{ suggestions: [] }">
    <input x-on:input.debounce="suggestions = await $wire.fetchSuggestions($event.target.value)">
</div>

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

Захист від гонитви на сервері асинхронність не скасовує: дві паралельні дії, що змінюють один запис у базі, потребують атомарних операцій (increment()), транзакцій чи блокувань, як і будь-які паралельні HTTP-запити.

Зв'язок з іншими механізмами:

  • з #[Renderless] - класичне поєднання для «запустив і забув»;
  • #[Isolate] - про групування між компонентами, а #[Async] - про чергу всередині компонента;
  • острівці теж ходять паралельно, і застереження про спільний стан для них те саме.

Докладніше в документації: Атрибут Async

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

persist: true - кеш для цього екземпляра компонента між його запитами (типово 3600 секунд):

#[Computed(persist: true, seconds: 600)]
public function stats()
{
    return Order::query()->where('shop_id', $this->shopId)->selectRaw('...')->first();
}

Користувач клацає фільтри, а важка статистика не перераховується на кожен клік.

cache: true - кеш спільний для всіх екземплярів компонента в застосунку:

#[Computed(cache: true, key: 'homepage-popular-posts', seconds: 300)]
public function popularPosts()
{
    return Post::popular()->limit(10)->get();
}

Одна й та сама головна сторінка для всіх - запит раз на п'ять хвилин замість кожного рендеру.

Головний ризик - cache: true з персональними даними:

#[Computed(cache: true)]   // помилка!
public function myOrders()
{
    return auth()->user()->orders()->latest()->get();
}

Ключ кешу не залежить від користувача. Перший, хто відкрив сторінку, «заповнить» кеш своїми замовленнями, а наступні побачать його дані. Це витік персональних даних, а не просто баг.

Правила:

  • cache: true - лише для даних, однакових для всіх (публічні списки, довідники, загальна статистика);
  • якщо дані залежать від користувача, мови, параметрів - ключ має їх містити (key: "orders-user-{$id}" неможливо в атрибуті, тож для таких випадків - звичайний Cache::remember у методі з явним ключем) або використовувати persist: true, прив'язаний до екземпляра;
  • інвалідація: кеш живе до закінчення терміну. Після зміни даних unset($this->stats) скидає лише кеш у межах запиту; для збережених значень потрібне явне очищення через кеш Laravel чи коротший термін;
  • серіалізація: у кеш потрапляють моделі й колекції - перевірте, що драйвер кешу й налаштування serializable_classes дозволяють такі об'єкти;
  • авторизація до кешу: перевірка прав має відбуватися до звернення до кешованого значення, а не всередині методу, який при влучанні в кеш не виконується.

Коли краще не використовувати ці опції: якщо запит швидкий (кілька мілісекунд) - звернення до кешу через мережу (Redis) може коштувати стільки ж, а складність і ризик застарілих даних додаються.

Докладніше в документації: Атрибут Computed

Інколи дія потрібна не для того, щоб змінити стан і перемалювати компонент, а щоб отримати дані для JavaScript: варіанти автодоповнення, точки для карти, дані для графіка. Атрибут #[Json] робить дію таким ендпойнтом.

use Livewire\Attributes\Json;

new class extends Component {
    #[Json]
    public function searchCities(string $query): array
    {
        return City::query()
            ->where('name', 'like', "{$query}%")
            ->limit(10)
            ->get(['id', 'name'])
            ->toArray();
    }
};
<div x-data="{ query: '', cities: [] }">
    <input x-model="query" x-on:input.debounce.300ms="cities = await $wire.searchCities(query)">

    <template x-for="city in cities" :key="city.id">
        <button type="button" x-text="city.name" x-on:click="$wire.cityId = city.id"></button>
    </template>
</div>

Що відрізняє #[Json]-дію:

  • повернене значення приходить у JavaScript як результат Promise від $wire.method();
  • помилки валідації не потрапляють у @error, а відхиляють Promise зі структурою { status: 422, errors: { поле: [...] } }:
try {
  const result = await $wire.save();
} catch (e) {
  if (e.status === 422) showErrors(e.errors);
}
  • компонент не перерендерюється заради результату - дані живуть в Alpine.

Навіщо це замість окремого API-маршруту:

  • не потрібні маршрут, контролер, CSRF-налаштування й автентифікація для API - дія працює в контексті компонента з його станом ($this->countryId) і поточним користувачем;
  • логіка лишається поруч із компонентом, який її використовує.

Що варто пам'ятати:

  • це публічний ендпойнт: будь-хто з доступом до сторінки може викликати метод з довільними аргументами. Обмежуйте результат (limit), перевіряйте права, не повертайте зайвих полів моделей (паролі, приховані атрибути - toArray() поважає $hidden, але явний get(['id', 'name']) надійніший);
  • частота: debounce на введенні обов'язковий, інакше кожна літера - запит;
  • паралельність: такі дії часто поєднують з #[Async], щоб вони не стояли в черзі за іншими діями компонента. Але тоді результат має зберігатися лише в JavaScript, не у властивостях;
  • для великих обсягів даних (тисячі точок) варто подумати про окремий кешований ендпойнт - відповідь дії Livewire теж містить знімок стану компонента.

Докладніше в документації: Атрибут Json

Кожна дія Livewire - це повний цикл на сервері: відтворення компонента зі знімка, хуки, дія, рендер шаблону, новий знімок. Повільно може бути на будь-якому етапі, тому спершу - виміряти.

Інструменти:

  • DevTools → Network, запити livewire/update: тривалість, розмір запиту й відповіді (великий знімок видно одразу), скільки компонентів у кожному запиті;
  • Laravel Debugbar / Telescope / Pulse - SQL-запити, їх кількість і дублікати на один запит Livewire;
  • профайлер (Xdebug, Blackfire, SPX) - де саме йде час: гідрація, дія чи рендер Blade.

Найчастіші причини:

1. Моделі й колекції в публічних властивостях. Відновлюються з бази на кожному запиті, без завантажених зв'язків - і шаблон отримує N+1. Рішення: обчислювані властивості з with().

2. Важкі запити в render(), boot(), hydrate(). Виконуються на кожен клік, навіть коли дія їх не стосується. Рішення: #[Computed] (виконується лише при зверненні), острівці, кешування.

3. Перерендер усього компонента заради дрібниці. Перемикач «улюблене» в рядку таблиці на 200 рядків перебудовує всю таблицю. Рішення: острівець, #[Renderless] для дій без зміни вигляду, окремий компонент для рядка.

4. Зайві запити від wire:model.live. Запит на кожну паузу у введенні в кожному полі. Рішення: звичайний wire:model (дані підуть з відправкою форми), .blur, більший debounce.

5. Великий знімок стану. Масиви на тисячі елементів, вкладені структури в публічних властивостях їдуть в обидва боки з кожним запитом. Рішення: тримати в стані лише ідентифікатори й значення фільтрів.

6. Багато компонентів на сторінці. Сотні дрібних компонентів у циклі - сотні знімків у HTML і накладні витрати на кожен. Рішення: Blade-компоненти для статичних частин, Livewire лише там, де справді потрібна інтерактивність.

7. Повільний компонент гальмує інші. Групування запитів змушує чекати найповільнішого. Рішення: #[Isolate], lazy/defer.

8. Опитування. wire:poll з коротким інтервалом на популярній сторінці множить навантаження на кількість відкритих вкладок.

Не лише сервер: повільний морфінг величезного DOM (тисячі рядків) теж відчутний - тут допомагають пагінація й віртуалізація, а не оптимізація PHP.

Перевірка після виправлення - ті самі вимірювання: кількість SQL-запитів на одну дію і розмір відповіді мають помітно зменшитися.

Докладніше в документації: Розв'язання проблем