У Blazor два способа исполнить один и тот же Razor-компонент, и цена выбора между ними выше, чем кажется при первом знакомстве. Blazor Server вышел вместе с .NET Core 3.0 осенью 2019 года: компоненты считаются на сервере, а в браузер поверх протокола WebSockets отправляются готовые изменения разметки. Blazor WebAssembly добрался до релиза в мае 2020 года и перевернул схему: в браузер скачиваются среда выполнения .NET, сборки приложения и зависимости, после чего код исполняется прямо на клиенте. С .NET 8 обе модели превратились из отдельных шаблонов проектов в режимы одного приложения Blazor Web App: страницу можно отдать статикой, можно держать интерактивной на сервере, можно целиком перенести в браузер. Выбор от этого не исчез, он просто спустился с уровня проекта на уровень страницы. Продакшен-цена решения складывается из трёх вещей: качество сети между пользователем и сервером, число одновременных визитов и то, где живут секреты и доступ к базе. Разберём обе модели без прикрас.

Две модели исполнения компонентов Blazor и их разная цена

В серверном режиме всё приложение живёт в ASP.NET Core: обработка событий, вызовы JavaScript, обновления интерфейса. Состояние каждого подключённого клиента хранится в памяти сервера и называется circuit. Браузер получает тонкий загрузчик и держит одно постоянное соединение поверх WebSockets, никакая среда выполнения клиенту не нужна.

В браузерном режиме всё наоборот: компоненты, зависимости и рантайм загружаются в браузер, а дальше приложение работает без сервера. Компоненты исполняются прямо в потоке интерфейса браузера, файлы раздаются как статика, и подойдёт любой хостинг статики или CDN. Загрузка выполняется параллельно, среда выполнения и сборки приезжают в браузер одновременно.

Существует и третий вариант, Blazor Hybrid: компоненты рендерятся в системном окне браузера внутри нативного приложения для десктопа или мобильного устройства. В споре веб-режимов он не участвует, но кодовая база компонентов переиспользуется и там, что делает выбор ещё менее драматичным.

Оба веб-варианта создаются одной строкой в консоли:

dotnet new blazor -o MyApp --interactivity Auto
dotnet new blazorwasm -o Catalog --pwa

Первая команда собирает унифицированное приложение Blazor Web App, где интерактивность можно менять от страницы к странице. Вторая создаёт автономное браузерное приложение с манифестом PWA: такое умеет работать офлайн, открываться в собственном окне и обновляться в фоне.

Внутренние панели и корпоративные кабинеты уверенно живут на Blazor Server

Панель склада, терминал оператора, кабинет службы поддержки: пользователи работают в одной сети с сервером, задержка до сервера измеряется миллисекундами, связь рвётся редко. Здесь Blazor Server раскрывается полностью. Загрузка тонкая, потому что рантайм не пересылается клиенту, приложение стартует быстро даже на старых ноутбуках. Секреты, строки подключения к базе и внутренние API остаются на сервере, браузеру достаётся только разметка. Полный стек .NET доступен целиком: чтение файлов, фоновые сервисы и библиотеки доступа к базе подключаются напрямую, отдельный слой HTTP-API для собственной панели писать не обязательно. Обновление релиза выкатывается на сервер, и через минуту все операторы работают в новой версии без выкачивания пакетов на каждый рабочий стол. Браузер в буквальном смысле работает терминалом: слабый компьютер не влияет на отзывчивость интерфейса, пока жив канал.

Панель на 200 одновременных операторов серверу почти ничего не стоит: 200 circuits это 200 легковесных соединений, а вычисления выполняются там же, рядом с базой. Клиентские машины при этом разгружены полностью, их вклад в работу интерфейса минимальный. Для внутренних инструментов аргументы звучат убедительно, и в продакшене такие панели годами живут в серверном режиме без нареканий.

Публичные интерфейсы под большой трафик и аргументы Blazor WebAssembly

Публичный каталог, витрина, онлайн-конфигуратор: посетителей тысячи, и каждый визит в серверном режиме означает живой circuit с состоянием в памяти. При 10 000 одновременных визитов сервер держит 10 000 соединений и 10 000 копий состояния, хотя большинству гостей нужна одна кнопка и пара полей. Сервер превращается в узкое место ровно там, где мог бы просто отдавать файлы.

Blazor WebAssembly меняет экономику. Браузер скачивает приложение, дальше сервер раздаёт статику или вообще не участвует, и её можно вынести на CDN. Хватит даже объектного хранилища со статикой, серверный бэкенд для витрины не нужен вовсе. Если сервер лёг, у клиентов приложение продолжает работать. PWA добавляет полный набор возможностей нативного клиента: офлайн-режим, собственное окно, запуск с домашнего экрана операционной системы, push-уведомления и автообновление в фоне. Сотрудник на выездном объекте без сети продолжает заполнять форму, а расчёты конфигуратора выполняет процессор клиента, а не ферма серверов. Для публичных интерфейсов с большой посещаемостью браузерный режим почти всегда оказывается честнее по деньгам и по надёжности.

Смешанные режимы .NET 8 и Interactive Auto для страниц с разной нагрузкой

С .NET 8 появилось главное послабление: внутри одного приложения страница может быть статичной, серверной и браузерной одновременно. Каталог рендерится статикой без интерактивности и остаётся быстрым для поисковых систем, личный кабинет работает на Interactive Server, а тяжёлый редактор выгрузок уезжает в браузер через Interactive WebAssembly. Интерактивные режимы включаются в Program-файле один раз:

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode();

Конкретная страница объявляет свой режим директивой:

@page "/orders"
@rendermode InteractiveServer

Статичные страницы не требуют ни постоянного соединения, ни клиентского рантайма, потому и грузятся быстрее всех.

Интерактивные режимы по умолчанию поддерживают предрендеринг: страница сначала отдаётся статикой с сервера, чтобы пользователь увидел содержимое до готовности интерактивной части. У предрендеринга есть обратная сторона: крупное предрендеренное состояние обязано уместиться во входящее сообщение при старте circuit, иначе схема не инициализируется и на экране появляется интерфейс переподключения. Есть и режим Interactive Auto: первый визит обрабатывается сервером, а когда браузер докачает пакет WebAssembly, следующие заходы исполняются уже на клиенте. Пользователь получает быстрый старт без ожидания мегабайтов, а сервер постепенно перестаёт быть единственным исполнителем. У смешанных решений своя цена: логика авторизации, вызовы JavaScript и доступ к API различаются между режимами, и страница, переехавшая из сервера в браузер, требует проверки обеих веток.

Ограничения Blazor Server задержка каждого клика и память circuits

Серверный режим держит интерактивность, пока жив канал связи, и его уязвимость видна в каждом факте. Каждое нажатие клавиши это сетевой прыжок: браузер отправляет событие, сервер обрабатывает, возвращает обновление разметки. На длинной дистанции или в мобильной сети задержки заметны глазу, офлайна нет вовсе, обрыв соединения останавливает интерактивность до переподключения. Арифметика проста: каждая итерация диалога стоит полного круга до сервера, и на канале с сотнями миллисекунд в одну сторону плавность интерфейса заканчивается. Circuit переживает короткие сбои сети, состояние не теряется при моргнувшем роутере, но долгий обрыв требует восстановления сессии, и платформа показывает пользователю специальный интерфейс ожидания. При нескольких серверах нужен session affinity, он же sticky sessions: circuit клиента живёт в памяти конкретной машины, и переподключение обязано попасть на неё же.

Мелкая, но больная деталь: входящее сообщение соединения по умолчанию ограничено 32 КБ, и объёмное состояние или крупная форма упираются в потолок. Лимит поднимается одной настройкой:

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddHubOptions(options =>
        options.MaximumReceiveMessageSize = 64 * 1024);

Документация прямо предупреждает, что рост лимита увеличивает поверхность для атак отказа в обслуживании, поэтому цифру поднимают осознанно. Параллельные вызовы на соединении ограничены единицей по умолчанию, платформа опирается на это при загрузке файлов, и повышать планку опасно. Для горизонтального масштабирования существуют управляемые облачные сервисы с поддержкой stateful reconnect, они снимают с фермы серверов значительную часть работы по поддержанию соединений. Строить наблюдаемость удобно из коробки: встроенные метрики показывают число активных circuits и время жизни сессий прямо в системе мониторинга через OpenTelemetry.

Ограничения Blazor WebAssembly вес первой загрузки и код на виду у всех

Браузерный режим платит за свободу весом и прозрачностью. Первая загрузка тянет за собой рантайм .NET и сборки приложения, старт заметно тяжелее серверного режима, и это чувствуют пользователи медленных сетей. От клиента требуется полноценная поддержка WebAssembly, устаревшие браузеры в игре не участвуют. Код, ушедший в браузер, невозможно спрятать от изучения и подмены, секретов в клиенте быть не может в принципе, доступ к внутренним сервисам закрывается отдельным API. IL исполняется интерпретатором, у которого в свежих версиях появилась частичная JIT-поддержка, а полноценную скорость дают только AOT-сборки в нативный WebAssembly, расплачивающиеся размером пакета.

Инструментарий сглаживает углы. При публикации вывод сжимается статически по алгоритмам Brotli и Gzip, хостингу остаётся отдать нужный файл по заголовку запроса. Лишний IL срезается триммером при каждой Release-сборке, повторные визиты оплачивает кэш браузера. Настройки задаются свойствами MSBuild:

<PropertyGroup>
  <RunAOTCompile>true</RunAOTCompile>
  <EmccMaximumHeapSize>268435456</EmccMaximumHeapSize>
</PropertyGroup>

Вторая строка взята из документации для мобильных браузеров: стандартный потолок кучи в 2 147 483 648 байт слишком велик для Safari на iOS, браузер отказывает в памяти, поэтому потолок занижают. Для счётных задач вроде расчёта геометрии или обработки изображений AOT даёт заметный прирост, для форм и таблиц интерпретатора хватает с запасом, и решение принимают по профилю нагрузки приложения.

Практический решающий критерий между двумя режимами Blazor

Сколько ни крутить, критерий один: где живёт состояние и кто платит за интерактивность, сервер или клиент. Быстрая проверка перед стартом выглядит так:
1. приложение внутреннее, пользователи в одной сети с сервером, секреты и база должны остаться на сервере, берите Blazor Server;
2. интерфейс публичный, одновременных визитов много, а серверных ресурсов на каждого гостя не предусмотрено, берите Blazor WebAssembly;
3. приложение смешанное, часть страниц статична, часть требует тяжёлой интерактивности, настраивайте режимы постранично средствами .NET 8;
4. нужен офлайн или работа вовсе без сервера, остаётся WebAssembly в форме PWA.

С .NET 8 выбор перестал быть решением на всю жизнь проекта: каталог можно отдать статикой, кабинет оставить на сервере, редактор унести в браузер, а Interactive Auto мягко переведёт пользователей с сервера на клиент по мере прогрева кэша. Но фундамент не изменился: серверный режим покупает тонкость клиента и доступ к внутренним ресурсам ценой живого соединения на каждого пользователя, браузерный покупает масштабируемость и офлайн ценой веса первой загрузки и публичности кода. Честный инженер оценивает трафик, сеть и требования к секретам до первой команды dotnet new, потому что переезд между режимами после релиза обходится дороже, чем час раздумий до него. Платформа дала гибкость, но выбирать по-прежнему приходится человеку.