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

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

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

4 питання

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

<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 на «маркері» внизу списку.

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