Виджеты в Yii 2: рабочие приёмы и где они бьют по производительности
Виджет удобно вынести в шаблон и забыть. Но GridView на 500 строк, вложенные виджеты и ActiveForm без кеша превращают удобство в лишние запросы и тормоза рендера. Разбираем приёмы, которые держат виджеты управляемыми.

Виджет в Yii 2 — это самый быстрый способ повторно использовать кусок интерфейса: вызвали Widget::widget(), получили HTML. Проблема начинается, когда виджетов становится много, они вкладываются друг в друга, а GridView отдаёт таблицу на пятьсот строк с колонкой-виджетом в каждой ячейке. Тогда удобная абстракция превращается в десятки лишних SQL-запросов и заметную задержку рендера.
Разберём приёмы работы с виджетами Yii 2 (актуально для yiisoft/yii2 2.0.x): когда писать свой виджет, а когда хватит partial-шаблона; как не плодить запросы в GridView; как передавать данные в виджет без сюрпризов. Внутренности View и механику ассетов трогаем только там, где это влияет на выбор.
Виджет или partial: что дороже
Первый вопрос перед созданием класса-виджета — а нужен ли он вообще. У Yii есть три способа переиспользовать разметку, и цена у них разная.
| Способ | Что делает | Цена |
|---|---|---|
$this->render('_block', $params) | Подключает partial-шаблон | Дёшево: один include PHP-файла, никакого объекта |
Свой класс Widget | Инкапсулирует логику + разметку | Создание объекта, вызов init() и run(), регистрация ассетов |
Встроенный виджет (GridView, ActiveForm) | Готовое сложное поведение | Может тянуть провайдер данных, ассеты, скрипты |
Правило простое: если блок — это только разметка с парой переменных, берите partial. Виджет оправдан, когда есть своя логика: разбор параметров, состояние, регистрация JS, генерация HTML в цикле. Класс ради вызова одного шаблона — лишняя абстракция.
Виджет — это код, который решает, как себя нарисовать. Partial — это разметка, которая уже знает, что рисует. Если решать нечего, класс не нужен.
Каркас своего виджета
Минимальный работающий виджет с публичными свойствами и шаблоном:
<?php
// widgets/AlertBox.php — Yii 2.0.x
namespace app\widgets;
use yii\base\Widget;
use yii\helpers\Html;
class AlertBox extends Widget
{
// Публичные свойства — это входные параметры виджета
public $type = 'info'; // info | success | danger
public $message = '';
public function init()
{
parent::init();
// Нормализуем вход один раз, а не в run()
if (!in_array($this->type, ['info', 'success', 'danger'], true)) {
$this->type = 'info';
}
}
public function run()
{
// Возвращаем строку, а не echo — так виджет можно и вывести, и присвоить
return $this->render('alertBox', [
'type' => $this->type,
'message' => $this->message,
]);
}
}Вызов в представлении:
<?= \app\widgets\AlertBox::widget([
'type' => 'danger',
'message' => 'Не удалось сохранить заказ',
]) ?>Обратите внимание: run() возвращает строку. Если внутри виджета делать echo, вы не сможете присвоить результат переменной или обернуть его — а это нужно чаще, чем кажется.
Приём 1: begin/end для виджетов с содержимым
Часть встроенных виджетов работает в парном режиме — ::begin() и ::end(). Так устроен ActiveForm: всё, что между вызовами, попадает внутрь формы. Свой виджет тоже так умеет, если в run() забрать буфер.
<?php $form = \yii\widgets\ActiveForm::begin([
'id' => 'order-form',
]) ?>
<?= $form->field($model, 'name') ?>
<?= $form->field($model, 'email') ?>
<?= \yii\helpers\Html::submitButton('Сохранить') ?>
<?php \yii\widgets\ActiveForm::end() ?>Типичная ошибка новичков — забыть ActiveForm::end(). Форма тогда не закроется, а буфер вывода останется незакрытым, и вы получите съехавшую вёрстку или обрезанный HTML в конце страницы. Держите begin и end визуально на одном уровне отступа.
Приём 2: GridView без лишних запросов
Самая частая точка боли — GridView, где каждая строка тянет связанные данные. Колонка вида «показать имя автора» без предзагрузки связи даёт классический N+1: один запрос на список плюс по запросу на каждую строку.
Лечится это не в виджете, а в провайдере данных — через with() на запросе:
<?php
// В контроллере или ActiveDataProvider
use yii\data\ActiveDataProvider;
use app\models\Order;
$dataProvider = new ActiveDataProvider([
// eager loading связи author — одна выборка вместо N
'query' => Order::find()->with('author'),
'pagination' => ['pageSize' => 50],
]);В самой колонке тогда обращение к $model->author->name уже не бьёт в базу — данные подгружены пачкой. Проверить это можно в панели отладки Yii (yii2-debug): вкладка Database покажет реальное число запросов на рендер страницы.
Колонка-виджет: осторожно
Соблазн — вставить в колонку GridView вызов своего виджета через value. На пятистах строках это пятьсот созданий объекта, пятьсот вызовов init() и run(). Если виджет ещё и регистрирует ассеты, Yii, к счастью, дедуплицирует их по имени бандла, но накладные расходы на объекты остаются.
- Для простого форматирования ячейки используйте замыкание в
valueилиformat— без виджета. - Виджет в ячейке оправдан, только если внутри действительно нетривиальная логика с состоянием.
- Если строк много, померьте: иногда дешевле собрать HTML строкой в замыкании, чем гонять объект на каждую строку.
Приём 3: не тяните ассеты, когда их не просят
Виджет может регистрировать свой JS и CSS через AssetBundle. Делайте это в init() или run(), но помните: если виджет вставлен в цикл, регистрация вызывается на каждой итерации. Yii не подключит один бандл дважды, однако лишние вызовы registerAssetBundle() — это работа, которую можно не делать.
<?php
public function run()
{
// Регистрируем ассет один раз через флаг на уровне View
$view = $this->getView();
if (!isset($view->params['_alertAssetDone'])) {
AlertAsset::register($view);
$view->params['_alertAssetDone'] = true;
}
return $this->render('alertBox', [/* ... */]);
}Это ручной приём. Для большинства случаев штатной дедупликации хватает — не усложняйте, пока замеры не покажут проблему.
Когда что выбирать
- Partial-шаблон — блок только из разметки, переменные передаются как есть, логики нет.
- Свой виджет — есть разбор параметров, состояние, генерация HTML в цикле, регистрация ассетов.
- begin/end — виджет оборачивает произвольное содержимое (форма, панель, модальное окно).
- Встроенный
GridView/ActiveForm— стандартные задачи; не переписывайте их своими виджетами без причины.
Что ломается чаще всего
- Забытый
end()у парного виджета — незакрытый буфер, битая вёрстка внизу страницы. - N+1 в
GridView— колонка тянет связь безwith(). Проверяйте вкладку Database в yii2-debug. - Тяжёлый виджет в ячейке — объект на каждую строку. Замените замыканием, где можно.
- Логика в шаблоне виджета вместо
init()/run()— параметры разбираются на каждый рендер, шаблон становится нечитаемым. echoвместоreturnвrun()— виджет нельзя присвоить или обернуть, а порядок вывода ломается при вложенности.
Что настроить дальше
Включите yii2-debug в dev-окружении и смотрите две вкладки на страницах с виджетами: Database (сколько запросов реально ушло) и Timeline (сколько времени занял рендер). Это единственный честный способ понять, где виджет стал дорогим. Инвалидация и прогрев фрагментного кеша (yii\widgets\FragmentCache) — отдельная тема; если рендер тяжёлого виджета повторяется, начните смотреть туда.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: Yii 2 Guide — Widgets, Yii 2 Guide — Data Widgets (GridView)
Частые вопросы
У меня Yii 2.0.14, а не последняя версия — приёмы сработают?
Что делать, если виджет выводит пустую строку вместо HTML?
GridView всё равно делает много запросов, хотя я добавил with(). Почему?
Можно ли вкладывать виджеты друг в друга?
Виджет заметно тормозит рендер в проде — с чего начать?
Стоит ли переписывать GridView своим виджетом ради контроля над разметкой?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.