Как Blazor управляет повторным рендерингом компонентов и как его оптимизировать?
Blazor рендерит компонент при изменении параметров или вызове StateHasChanged(). Оптимизация: переопределить ShouldRender(), использовать @key в списках, компонент Virtualize для длинных коллекций и разбивку на мелкие изолированные компоненты.
Управление повторным рендерингом в Blazor
Blazor автоматически перерендеривает компонент при изменении параметров или вызове StateHasChanged(). Это удобно, но при сложном дереве компонентов неоптимальные ре-рендеры становятся узким местом производительности.
Когда происходит ре-рендер
- Вызван
StateHasChanged()(явно или через EventCallback). - Изменились входящие параметры (
SetParametersAsync). - Родительский компонент перерендерился (по умолчанию тянет дочерние).
ShouldRender() — ручной контроль
Переопределите ShouldRender(), чтобы пропускать ненужные ре-рендеры:
private bool _forceRender = false;
protected override bool ShouldRender()
{
if (_forceRender)
{
_forceRender = false;
return true;
}
return false; // Рендеримся только по явному запросу
}
private void OnImportantChange()
{
_forceRender = true;
StateHasChanged();
}
ComponentBase vs IComponent — разница в частоте рендеров
ComponentBase рендерится дважды при изменении параметров: один раз синхронно, второй после await. Если хотите один рендер — используйте флаг или реализуйте IComponent напрямую (редкий сценарий).
Мемоизация через @key
При рендеринге списков используйте @key чтобы Blazor переиспользовал DOM-узлы, а не пересоздавал их:
@foreach (var job in Jobs)
{
<JobCard @key="job.Id" Job="job" />
}
EventCallback автоматически вызывает StateHasChanged
EventCallback у родителя вызывает StateHasChanged автоматически. Если нужно избежать лишних ре-рендеров, используйте Action вместо EventCallback, но тогда вам самим нужно управлять обновлением UI.
Виртуализация длинных списков
<Virtualize Items="AllJobs" Context="job" OverscanCount="5">
<ItemContent>
<JobCard Job="job" />
</ItemContent>
<Placeholder>
<div class="skeleton">Loading...</div>
</Placeholder>
</Virtualize>
Разбивка на мелкие компоненты
Выносите «дорогие» части UI в отдельные компоненты и переопределяйте в них ShouldRender(). Так ре-рендер родителя не затрагивает изолированный дочерний компонент.
StateHasChanged из фонового потока
// Правильно — через InvokeAsync
await InvokeAsync(StateHasChanged);
// Неправильно — прямой вызов из другого потока может вызвать исключение
StateHasChanged(); // опасно вне UI-потока
Подводные камни
- Двойной рендер ComponentBase. После каждого async-метода жизненного цикла Blazor рендерится повторно. Используйте ShouldRender(), чтобы подавить промежуточные рендеры.
- EventCallback вызывает рендер родителя. Даже если данные не изменились, родитель перерендерится. При высокочастотных событиях (mousemove, scroll) это критично — используйте дебаунс или Action.
- Отсутствие @key в списках. Без @key при сортировке/фильтрации Blazor пересоздаёт DOM-элементы вместо их перемещения — это медленно и ломает анимации.
- Глубокое дерево и параметры-объекты. Если передаётся объект по ссылке, Blazor сравнивает ссылки, а не значения. Неизменённый объект всё равно вызовет ре-рендер если ссылка новая (например, при LINQ ToList()).
- CascadingValue без IsFixed. Каждое изменение каскадного значения вызывает ре-рендер всех потребителей. Для статичных значений устанавливайте IsFixed="true".
- Virtualize и высота элементов. Virtualize требует фиксированной высоты или ItemSize. Динамические высоты нарушают расчёт видимой области.
- ShouldRender не вызывается при первом рендере. Метод вызывается только при последующих обновлениях, не при первоначальной отрисовке компонента.
Common mistakes
- Путать render tree diffing и оптимизация повторного рендеринга с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Blazor: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет render tree diffing и оптимизация повторного рендеринга на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.