Livewire: реактивность без отдельного фронтенд-фреймворка
Как Livewire даёт живой интерфейс в Laravel без сборки Vue или React — на чём это работает, сколько стоит в запросах и где ломается.

Форма с валидацией по мере ввода, таблица с фильтрами без перезагрузки, счётчик корзины, обновляющийся при клике — всё это в Laravel обычно означает отдельный SPA на Vue или React, свой роутинг, сборку через Vite и API-слой к бэкенду. Livewire убирает этот второй проект: компонент — это PHP-класс и Blade-шаблон, а «реактивность» строится на HTTP-запросах к серверу, который перерисовывает разметку. Разберём, что происходит под капотом каждого действия пользователя, чем вы за это платите в трафике и задержках, и где модель начинает трещать.
Материал написан под Livewire 3.x в связке с Laravel 10/11. Livewire 3 заметно отличается от версии 2: сменился неймспейс директив (wire: остался, но появился Alpine под капотом), поменялся способ подключения ассетов и формат hydration. Если вы на второй версии — часть примеров не совпадёт по синтаксису.
Как это вообще работает
Livewire-компонент живёт в двух местах: класс-наследник Livewire\Component с публичными свойствами и методами, и Blade-шаблон, привязанный к нему. При первой загрузке страницы сервер рендерит компонент как обычный HTML и вшивает в него сериализованное состояние — снимок публичных свойств (snapshot).
Дальше начинается интересное. Когда пользователь что-то делает — печатает в поле с wire:model, кликает по кнопке с wire:click — Livewire отправляет на сервер AJAX-запрос. В нём летят: текущий снимок состояния, вызванное действие и подписанный чек-сумм, чтобы состояние нельзя было подделать. Сервер восстанавливает объект компонента из снимка (это называется hydration), выполняет метод, заново рендерит Blade и возвращает не весь HTML, а diff — что изменилось. На клиенте JavaScript-рантайм Livewire применяет этот diff к DOM через morphdom, не перезагружая страницу.
Hydration / dehydration — восстановление PHP-объекта компонента из сериализованного снимка перед обработкой запроса и обратная упаковка после. Каждый цикл взаимодействия компонент рождается заново: между двумя кликами объекта в памяти сервера не существует.
Ключевой факт, который многие упускают: между двумя действиями пользователя ваш компонент не живёт. Каждый клик — это новый экземпляр класса, собранный из снимка. Всё, что вы положили в приватное свойство или посчитали в конструкторе, к следующему запросу исчезнет, если это не публичное свойство.
Простой компонент целиком
Счётчик — банально, но показывает весь цикл. Создаётся командой php artisan make:livewire Counter, которая генерирует класс и шаблон.
<?php
// app/Livewire/Counter.php (Livewire 3)
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
// Публичное свойство попадает в снимок и синхронизируется
public int $count = 0;
public function increment(): void
{
$this->count++;
}
public function render()
{
return view('livewire.counter');
}
}<!-- resources/views/livewire/counter.blade.php -->
<div>
<span>{{ $count }}</span>
<button wire:click="increment">+1</button>
</div>Обратите внимание на обёртку <div>: у Livewire-шаблона должен быть один корневой элемент. Два соседних тега на верхнем уровне — и morphdom не поймёт, к чему применять diff. Разместить компонент на странице можно так: <livewire:counter />.
Что стоит за каждым кликом
Кнопка «+1» выглядит как локальное действие, но по сети это полноценный round-trip: запрос со снимком уходит на сервер, там поднимается Laravel-приложение, восстанавливается компонент, вызывается метод, перерисовывается Blade, назад летит diff. На локалхосте это 20–40 мс, на реальном канале с задержкой до сервера — легко 150–300 мс на действие.
Отсюда первое правило дисциплины: wire:model на поле ввода по умолчанию в Livewire 3 работает в режиме deferred — значение уходит на сервер не на каждое нажатие клавиши, а при следующем действии (submit, клик). Если вам нужна реакция прямо во время набора, добавляйте модификатор явно:
wire:model="search"— отложенно, значение синхронизируется при следующем сетевом запросе. Дёшево.wire:model.live="search"— запрос на каждое изменение. Живой поиск, но каждая буква — round-trip.wire:model.live.debounce.300ms="search"— живой, но с паузой 300 мс после последнего нажатия. Обычно то, что нужно для поиска.
Забыли debounce на живом поле — и поле поиска шлёт по запросу на символ. Пользователь печатает «ноутбук», сервер получает семь запросов, каждый тянет выборку из БД. Это не гипотетика, а самая частая причина, по которой Livewire-страница «тормозит».
Три способа обновлять только часть страницы
Когда компонент разрастается, перерисовка всего шаблона на каждый чих становится дорогой. Livewire даёт несколько инструментов ограничить область работы.
| Приём | Что делает | Цена |
|---|---|---|
wire:model.blur | Синхронизирует поле при потере фокуса, а не при вводе | Реакция не мгновенная, но один запрос вместо десятков |
Computed property (#[Computed]) | Кеширует результат метода в пределах одного запроса | Кеш живёт только внутри цикла, между запросами пересчитывается |
Вложенный компонент + wire:key | Изолирует часть страницы в свой цикл hydration | Сложнее передавать состояние между родителем и ребёнком |
Computed property заменяет свойство, которое дорого вычислять и не нужно хранить в снимке:
use Livewire\Attributes\Computed;
#[Computed]
public function results()
{
// Выполнится один раз за запрос, повторный вызов $this->results вернёт кеш
return Product::where('name', 'like', "%{$this->search}%")->get();
}Атрибут #[Computed] появился в Livewire 3. В шаблоне обращение идёт как к свойству — @foreach($this->results as $product), без скобок. Важно: результат не попадает в снимок и не гоняется по сети, в отличие от публичного свойства. Тяжёлую выборку из БД держите здесь, а не в public array $results.
Где модель ломается
Livewire отлично закрывает 80% интерактива в админках, дашбордах и формах. Оставшиеся 20% — это места, где серверный round-trip на каждое действие становится либо медленным, либо абсурдным.
- Высокочастотные события. Drag-and-drop, перетаскивание элементов, рисование, что угодно с событием на каждый пиксель. Отправлять запрос на mousemove нельзя. Здесь либо Alpine.js локально (он идёт в комплекте с Livewire 3), либо честный фронтенд-фреймворк.
- Большое состояние в снимке. Публичное свойство с массивом из тысячи строк сериализуется и гоняется туда-обратно на каждый клик. Diff помогает с DOM, но снимок улетает целиком. Держите крупные данные в computed property или подгружайте страницами.
- Оффлайн и мгновенный отклик. Нет соединения — нет реактивности. Если интерфейс должен работать без сети, Livewire не про это.
- Проблема N+1 внутри рендера. Каждый цикл заново дёргает
render(). Если в шаблоне вы обращаетесь к связям модели без eager loading, N+1 бьёт на каждом клике, а не один раз при загрузке страницы. Симптом — сотни запросов к БД в Debugbar на одно нажатие.
Проверка состояния глазами
Поскольку компонент пересоздаётся каждый запрос, самая частая ошибка новичков — надежда, что значение «просто сохранится». Правило: если данные нужны между действиями пользователя — это публичное свойство сериализуемого типа (скаляр, массив, Eloquent-модель через встроенную поддержку). Приватное свойство, статик, результат в конструкторе — всё это обнулится.
Проверить, что улетает по сети, просто: откройте вкладку Network в браузере, кликните по элементу и посмотрите тело запроса к /livewire/update. Там будет весь снимок. Если видите там мегабайт данных — вы храните в публичных свойствах то, что должно быть computed.
Что делать дальше
Первое — поставьте Laravel Debugbar и следите за числом SQL-запросов на действие: рост при кликах означает N+1 в рендере, лечится with() на запросе. Второе — пройдитесь по всем wire:model.live и добавьте debounce там, где это поле ввода текста. Третье — если чувствуете, что боретесь с фреймворком на каждой интерактивной мелочи, посмотрите в сторону Alpine.js для чисто клиентских эффектов: он уже подключён и снимает нагрузку с сервера там, где ходить на бэкенд незачем.
Разрушитель авто-разводов: Фото заказ-наряда СТО: завышения, дубли работ, лишние позиции.
Спросить за 15 ₽Источники: Livewire 3 Documentation, Livewire GitHub Repository
Частые вопросы
У меня Livewire 2, примеры не совпадают. Что важно знать?
Почему поле поиска не реагирует на ввод?
Livewire тормозит при кликах — что смотреть первым?
Можно ли обойтись без сборки через Vite и npm?
Почему у шаблона должен быть один корневой элемент?
Подходит ли Livewire для интерфейса без интернета?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.