Junior: питання на співбесіді з теми «Продуктивність Livewire»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Після кожного запиту 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, коли статус фінальний.
Компонент з повільним запитом (статистика, звіт, зовнішній 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). Дані, що з них виводяться, - обчислювані властивості.