Blazor Server выполняет ваши компоненты на сервере и отправляет обновления интерфейса в браузер через соединение SignalR: каждая открытая вкладка - это живая цепь (circuit), хранящая состояние в памяти сервера, каждый клик - это путь туда и обратно, а память сервера растёт с числом одновременных пользователей, а не с числом запросов. Blazor WebAssembly загружает runtime .NET и ваше приложение в браузер и выполняет их там: сервер отдаёт только статические файлы и API, но первый визит стоит загрузки в несколько мегабайт, и ничто на клиенте не может быть секретным. Начиная с .NET 8 больше не нужно выбирать что-то одно для всего приложения - модель Blazor Web App по умолчанию рендерит страницы статически на сервере и позволяет каждому компоненту выбрать интерактивность Server, WebAssembly или Auto.
Для хостинга вопрос простой: сколько людей будет подключено одновременно, как далеко они от сервера и может ли сервер позволить себе помнить их всех? В этой статье для каждого из этих пунктов приведены цифры.
Режимы рендеринга в одной таблице#
Blazor Web App (шаблон по умолчанию начиная с .NET 8) поддерживает четыре способа рендерить компонент:
| Режим рендеринга | Где выполняется код | Интерактивность | Затраты сервера на посетителя |
|---|---|---|---|
| Статический серверный (SSR) | Сервер, на каждый запрос | Нет (только формы и ссылки) | Обычный HTTP-запрос |
InteractiveServer | Сервер, в цепи | Да | Живая цепь на весь визит |
InteractiveWebAssembly | Браузер | Да | Статические файлы, затем вызовы API |
InteractiveAuto | Сначала сервер, потом браузер | Да | Цепь, пока runtime не закэширован |
Статический SSR легко упустить из виду, а он часто и есть правильный ответ. Странице, которая показывает данные и отправляет форму, интерактивность вообще не нужна; она рендерится как Razor Page и ничего не стоит серверу после отправки ответа. Улучшенная навигация делает переходы между статическими страницами похожими на одностраничное приложение, а [StreamRendering] позволяет медленной странице сначала отправить макет, а данные - когда они будут готовы.
Интерактивность задаётся для отдельного компонента или для всего приложения:
builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents();app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);Затем компонент объявляет @rendermode InteractiveServer (или один из других режимов), и за интерактивность платят только этот компонент и его дочерние. Старых отдельных шаблонов Blazor Server и WebAssembly «ASP.NET Core hosted» начиная с .NET 8 больше нет; существующие приложения на их основе продолжают работать.
Как работает Blazor Server#
Когда загружается страница с интерактивным серверным компонентом, браузер открывает обратно к серверу соединение SignalR, предпочтительно через WebSocket. Сервер создаёт цепь: экземпляр каждого интерактивного компонента на странице, их состояние, дерево рендеринга и scoped-контейнер внедрения зависимостей. С этого момента:
- Пользователь нажимает кнопку. Браузер отправляет событие по соединению.
- Сервер выполняет обработчик события, заново рендерит затронутые компоненты и вычисляет разницу с последним рендером.
- Сервер отправляет разницу обратно, и браузер обновляет DOM.
Вот почему Blazor Server ощущается как написание настольного приложения: код может обращаться к базе данных напрямую, слой API строить не нужно, и ничего чувствительного никогда не попадает в браузер. И вот почему его затраты не похожи на затраты обычного веб-приложения. Цепь живёт, пока открыта вкладка, делает пользователь что-нибудь или нет, и scoped-сервисы в ней - включая DbContext EF Core, если вы внедряете его напрямую, - живут столько же.
Память и CPU на пользователя#
Собственные рекомендации Microsoft называют минимум примерно в 250 КБ памяти сервера на подключённого пользователя для очень простого приложения. Реальные приложения держат гораздо больше: поля каждого компонента, все загруженные в них данные, таблицы с тысячами строк и scoped-сервисы. Цепь, которая держит таблицу на 5000 строк с данными, измеряется мегабайтами и держит их весь визит.
Грубый способ оценки: измерьте одну цепь. Откройте приложение в одной вкладке, запомните график памяти, откройте ещё двадцать вкладок на самой тяжёлой странице и разделите прирост на двадцать. Затем умножьте на число людей, у которых, по вашим ожиданиям, приложение будет открыто в один и тот же момент, - не за день, а одновременно.
| Одновременных пользователей | Лёгкие страницы (около 0,5 МБ каждая) | Тяжёлые страницы (около 5 МБ каждая) |
|---|---|---|
| 20 | 10 МБ | 100 МБ |
| 200 | 100 МБ | 1 ГБ |
| 1 000 | 500 МБ | 5 ГБ |
Эти цифры на пользователя иллюстративны, а не измерены на вашем приложении; суть в форме зависимости. Приложение Blazor Server, которое прекрасно справляется с целым офисом пользователей, может не подойти для публичного сайта, куда одновременно могут прийти тысячи. Базовая память самого процесса сверх цепей описана в статье память и сборка мусора в .NET.
Параметры цепи, которые это ограничивают:
Настройка CircuitOptions | По умолчанию | Что она делает |
|---|---|---|
DisconnectedCircuitRetentionPeriod | 3 минуты | Сколько оборванная цепь хранится для переподключения |
DisconnectedCircuitMaxRetained | 100 | Сколько оборванных цепей хранится одновременно |
JSInteropDefaultCallTimeout | 1 минута | Таймаут вызовов в JavaScript |
MaxBufferedUnacknowledgedRenderBatches | 10 | Пакеты рендеринга в очереди для медленного клиента |
DetailedErrors | false | Отправлять подробности исключений в браузер - только для разработки |
По возможности не держите данные в компонентах: загружайте страницу результатов, а не всю таблицу. Используйте Virtualize для длинных списков, чтобы рендерились только видимые строки. Создавайте DbContext на каждую операцию через IDbContextFactory<T>, а не внедряйте один на всё время жизни цепи.
CPU обычно меньшая проблема, потому что большинство событий дешёвые. Он становится важен, когда многие пользователи одновременно запускают дорогие рендеры: дашборд, который перерисовывается по таймеру для каждого подключённого пользователя, - нагрузка, растущая вместе с аудиторией, даже когда никто ничего не трогает.
Задержка и соединение#
Каждое взаимодействие в Blazor Server ждёт сеть. При 20 мс между пользователем и сервером кнопка срабатывает мгновенно. При 150 мс ввод в поле, которое перерисовывается на каждое нажатие клавиши, кажется вязким, а drag-and-drop - сломанным. Если ваши пользователи на другом континенте относительно сервера, протестируйте оттуда, прежде чем выбирать режим Server. Что эти цифры значат на практике, объясняет статья задержка, джиттер и потеря пакетов.
Соединение к тому же должно выживать. Blazor Server нужно постоянное соединение через каждый прокси между браузером и Kestrel:
- WebSocket должен проходить апгрейд. Если не может, SignalR откатывается на long polling, который работает, но замедляет каждое событие и стоит больше запросов. Когда WebSocket работает, вкладка сети в браузере показывает ответ
101 Switching Protocols. - Таймауты простоя на прокси должны быть длиннее keep-alive у SignalR, который по умолчанию отправляет ping каждые 15 секунд.
- Приложение должно видеть настоящего клиента и схему через проксированные заголовки - см. статью ASP.NET Core за обратным прокси.
Когда соединение обрывается, браузер показывает оверлей переподключения и пробует снова. Если переподключиться удаётся в пределах периода хранения, цепь продолжает с того же места. Если нет, состояние потеряно, и пользователю придётся перезагрузить страницу. Мобильные пользователи, переключающиеся между сетями, и ноутбуки, просыпающиеся из сна, сталкиваются с этим постоянно; в .NET 9 и 10 переподключение заметно улучшили, а .NET 10 добавляет способ сохранять состояние цепи, чтобы пользователь мог продолжить после более долгого перерыва. Проверьте, что поддерживает ваша версия, прежде чем на это полагаться.
Как работает Blazor WebAssembly#
Режим WebAssembly отправляет ваше приложение в браузер. Первый визит загружает runtime .NET, скомпилированный в WebAssembly, используемые приложением сборки фреймворка и ваши собственные сборки, а затем выполняет всё локально. После этого браузер кэширует файлы, и последующие визиты запускаются быстро.
Компромиссы зеркальны режиму Server:
- Загрузка. Опубликованное приложение обрезается (для WebAssembly trimming включён по умолчанию) и сжимается, но рассчитывайте на объём первой загрузки, измеряемый мегабайтами, а не килобайтами: небольшое приложение занимает где-то 2-4 МБ в сжатом виде, более крупное - больше. На медленном мобильном соединении это секунды экрана загрузки.
- Никаких секретов. Всё на клиенте пользователь может прочитать. Строкам подключения, API-ключам и бизнес-правилам, которые нельзя обойти, место на сервере, за API.
- Скорость выполнения. По умолчанию ваш код интерпретируется runtime WebAssembly. Компиляция ahead-of-time (
<RunAOTCompilation>true</RunAOTCompilation>, для сборки нужен workloadwasm-tools) делает код, нагружающий CPU, гораздо быстрее, а загрузку - значительно больше. - Затраты сервера. Для интерфейса почти никаких. Сервер отдаёт статические файлы один раз на пользователя, а затем отвечает на вызовы API, которые масштабируются как любой другой API.
Хостинг приложения WebAssembly#
Отдельное приложение Blazor WebAssembly (шаблон blazorwasm) публикуется в папку статических файлов под wwwroot, и отдавать её может любой статический хостинг. Работает ли оно, зависит от трёх деталей:
- Резервная маршрутизация. Blazor обрабатывает маршруты в браузере, так что запрос
/orders/42должен возвращатьindex.html, а не 404. Статическому хостингу нужно правило перезаписи для неизвестных путей; как его добавить, зависит от сервера. Частые случаи разобраны в статье хостинг статических сайтов. - Сжатие и MIME-типы. Публикация создаёт заранее сжатые копии
.brи.gzкаждого файла. Отдача этих копий и отдача.wasmкакapplication/wasm- это разница между первой загрузкой в 3 МБ и в 10 МБ. - Заголовки кэширования. Файлы runtime снабжены отпечатками в именах, поэтому их можно кэшировать надолго;
index.htmlкэшировать нельзя, иначе после деплоя пользователи продолжат загружать старые версии. Обоснование - в статье заголовки HTTP-кэширования простыми словами.
API, который вызывает клиент, - отдельное приложение ASP.NET Core. Если оно на другом origin, чем статические файлы, настройте CORS на API. В Blazor Web App с компонентами InteractiveWebAssembly сервер ASP.NET Core отдаёт и клиентские файлы, и API с одного origin, что полностью избавляет от CORS и является самой простой схемой для хостинга.
Пререндеринг и двойная загрузка#
Интерактивные компоненты в Blazor Web App по умолчанию пререндерятся: сервер один раз рендерит HTML, чтобы страница появилась сразу и поисковые системы могли её прочитать, затем компонент запускается снова в интерактивном режиме и рендерится второй раз. Поэтому любая загрузка данных в OnInitializedAsync выполняется дважды - один раз при пререндеринге и ещё раз, когда управление берёт цепь или runtime WebAssembly.
Справиться с этим можно двумя способами:
- Сохранить состояние между двумя рендерами.
PersistentComponentState(начиная с .NET 8) сериализует данные из пререндера в страницу и передаёт их интерактивному рендеру. .NET 10 добавляет декларативный атрибут[PersistentState]на свойство компонента, который делает то же самое с меньшим количеством кода. - Выключить пререндеринг для компонентов, где первая отрисовка не важна, через
@rendermode @(new InteractiveServerRenderMode(prerender: false)). Тогда страница ничего не показывает на месте этого компонента, пока не запустится интерактивность.
Пререндеринг также означает, что код в OnInitializedAsync выполняется на сервере даже для компонента WebAssembly, поэтому он не должен предполагать, что находится в браузере, - никаких вызовов IJSRuntime там.
Деплои, перезапуски и потерянные цепи#
Деплой перезапускает процесс, и вместе с ним умирает каждая цепь Blazor Server. Пользователи видят оверлей переподключения, переподключение не удаётся, потому что знакомого им сервера больше нет, и им приходится перезагружать страницу, теряя всё несохранённое. Если включён деплой при push, каждый push в ветку делает это с каждым подключённым пользователем.
Поэтому для приложения Blazor Server: сохраняйте пользовательский ввод часто, деплойте в спокойное время и не используйте цепь как хранилище. Приложения WebAssembly гораздо снисходительнее: интерфейс продолжает работать в браузере, пока API перезапускается, и неудачными оказываются только вызовы, сделанные в этот промежуток. Чего можно и чего нельзя избежать с одним экземпляром, описано в статье деплой без простоя на небольшом сервере.
Ещё одно, что должно переживать перезапуск, - ключи Data Protection, используемые для antiforgery-токенов и cookie аутентификации. Если их сгенерировать заново, разлогинится каждый пользователь. Где их хранить, показано в статье деплой приложения ASP.NET Core с GitHub.
Как выбрать#
- В основном контент и формы: статический SSR, с интерактивностью только у тех компонентов, которым она нужна. Самый дешёвый вариант для хостинга, с большим отрывом.
- Внутренние инструменты и админки с десятками пользователей рядом с сервером:
InteractiveServer. Не нужно строить API, быстрая первая загрузка, а память в таком масштабе не проблема. - Публичные приложения с множеством одновременных пользователей или пользователями далеко от сервера:
InteractiveWebAssemblyс API. Сервер масштабируется по запросам, а не по открытым вкладкам. - Нужна быстрая первая загрузка, а после неё WebAssembly:
InteractiveAuto, с тем условием, что теперь вы пишете компоненты, которые должны работать в обоих местах.
Разобранный пример делает это разделение наглядным. У сайта бронирования спортивного клуба есть публичное расписание, форма бронирования и экран для персонала, где управляют кортами. Расписание - статический SSR с потоковым рендерингом: его читают гораздо чаще, чем всё остальное, и держать его открытым ничего не стоит. Форма бронирования - тоже статический SSR: обычной отправке формы с валидацией цепь не нужна. Экран персонала, которым пользуются четыре человека в собственной сети клуба, - InteractiveServer: богатый, мгновенный, работающий прямо с базой данных. В итоге всё спокойно работает на небольшом тарифе, потому что существуют только цепи тех четырёх людей, которым они приносят пользу. Тот же сайт, целиком построенный в режиме Server, держал бы цепь для каждого члена клуба, который в субботу утром смотрит расписание.
FAQ#
Сколько пользователей выдержит приложение Blazor Server на 2 ГБ?
Это целиком зависит от того, что держит каждая цепь. Лёгкое приложение с полумегабайтом на пользователя вместит несколько сотен одновременных пользователей помимо самого процесса; приложение, нагруженное данными, может справиться с несколькими десятками. Измерьте одну цепь и умножьте.
Нужен ли Blazor WebAssembly вообще .NET-сервер?
Для интерфейса - нет. Отдельное приложение WebAssembly - это статические файлы, и их может отдавать любой статический хостинг. .NET-сервер нужен только для API, который оно вызывает, а он у вас почти всегда будет.
Почему моё приложение Blazor Server отключается каждую минуту?
Таймаут простоя на прокси короче интервала keep-alive, или WebSocket не проходит, и резервный транспорт постоянно упирается в таймаут. Посмотрите во вкладке сети, какой транспорт используется.
Можно ли масштабировать Blazor Server на несколько серверов?
Да, с sticky sessions, чтобы соединение каждого пользователя всегда попадало на сервер, где хранится его цепь. На одном сервере этот вопрос не возникает; масштабирование для обычного SignalR описано в статье приложения реального времени на SignalR.
InteractiveAuto - это лучшее из обоих миров?
Это хороший вариант по умолчанию для публичных приложений, но каждый компонент Auto должен работать и на сервере, и в браузере, поэтому не может обращаться к базе данных напрямую. Это ограничение - цена быстрой первой загрузки.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.