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

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, коли статус фінальний.

Докладніше в документації: 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). Дані, що з них виводяться, - обчислювані властивості.

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