Start Debugging

Scalar vs Swagger UI para documentación OpenAPI en ASP.NET Core 11

Scalar envía 1.02 MiB de JavaScript comprimido con gzip y un constructor de solicitudes mucho mejor. Swagger UI envía 514 KiB y renderiza OpenAPI 3.2, que es lo que .NET 11 ya emite por defecto. Payloads medidos, la brecha de 3.2, enrutamiento por endpoints en ambos lados y los detalles de autenticación que deciden.

Elige Scalar (Scalar.AspNetCore 2.16.20) para una API nueva en .NET 11 si quienes leen tu documentación son externos, porque el constructor de solicitudes, los ejemplos de código en varios lenguajes y la búsqueda son realmente mejores que cualquier cosa que haga Swagger UI. Elige Swagger UI (Swashbuckle.AspNetCore.SwaggerUI 10.2.3, que empaqueta swagger-ui 5.32.7) si quieres el payload más pequeño, si dependes del flujo de redirección OAuth2 que ya configuraste, o si necesitas renderizado confiable de OpenAPI 3.2 hoy, porque .NET 11 emite 3.2 por defecto y el trabajo de 3.2 en Scalar sigue siendo un issue abierto. Ambos tienen licencia MIT, ambos son renderizadores puros sin voz ni voto sobre tu documento OpenAPI, y la guía de Microsoft es que ninguno debería ser accesible en producción.

Todo lo medido a continuación se ejecutó contra el SDK de .NET 10.0.201 con las versiones exactas de paquetes que se nombran, el 2026-08-15. La superficie de API es idéntica en .NET 8 hasta .NET 11, porque ambos paquetes publican ensamblados net8.0, net9.0 y net10.0 y toman una referencia de framework a Microsoft.AspNetCore.App en lugar de fijar un runtime.

La comparación que la gente cree estar haciendo no es la que importa

Desde .NET 9, dotnet new webapi no incluye Swashbuckle. Microsoft.AspNetCore.OpenApi genera el documento y es compatible con trimming y Native AOT. Eso significa que la decisión que tienes delante no es “Swashbuckle o Scalar”, sino “qué bundle de JavaScript renderiza el documento que tu framework ya produce”. Si sigues usando SwaggerGen de Swashbuckle para la generación, esa es una decisión aparte, cubierta en cómo exponer OpenAPI sin Swashbuckle en ASP.NET Core 11.

Esta distinción tiene una consecuencia práctica. Swashbuckle.AspNetCore, el metapaquete, arrastra Swashbuckle.AspNetCore.Swagger, SwaggerGen y Microsoft.Extensions.ApiDescription.Server junto con la interfaz. Si solo quieres la interfaz, referencia Swashbuckle.AspNetCore.SwaggerUI directamente y no viene nada más con ella.

<!-- .NET 11, C# 14: the UI only, no second document generator -->
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="11.0.0" />
  <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
</ItemGroup>
<!-- .NET 11, C# 14: the Scalar equivalent, one package, zero NuGet dependencies -->
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="11.0.0" />
  <PackageReference Include="Scalar.AspNetCore" Version="2.16.20" />
</ItemGroup>

La matriz

Scalar 2.16.20Swagger UI 5.32.7 (Swashbuckle 10.2.3)
Bytes en red en la primera carga (gzip)1 071 277526 322
JavaScript parseado tras descomprimir3 711 KB1 794 KB
Registroapp.MapScalarApiReference()app.UseSwaggerUI(...) o app.MapSwaggerUI(...)
Enrutamiento por endpointsSí, desde 1.xSí, desde 10.2.0 (mayo de 2026)
OpenAPI 3.2El parser lo maneja, soporte completo en un issue abiertoSoporte básico desde swagger-ui 5.32.0
Ejemplos de códigoMás de 20 destinos (curl, fetch, axios, Python, Go, Java, PHP, Ruby y más)curl para la solicitud que acabas de enviar
Caché de assetsCache-Control: no-cache más ETag, fijo en el códigoETag por defecto, max-age si configuras CacheLifetime
Credenciales persistidaspersistAuth escribe en local storagePersistAuthorization en el objeto de configuración
Try It entre orígenesproxyUrl opcionalfetch directo del navegador, CORS es tu problema
Temas12 temas integrados, customCss, pluginsInjectStylesheet, InjectJavascript, el sistema de plugins de swagger-ui
LicenciaMITMIT

Lo que cuesta cada uno al navegador, medido

Ambos paquetes incrustan sus assets en el ensamblado como streams gzip y entregan esos bytes directamente a un cliente que anuncia Accept-Encoding: gzip. La integración de Scalar con ASP.NET Core comprueba IsGzipAccepted() y establece Content-Encoding más Vary: Accept-Encoding a partir del asset almacenado. El middleware de la interfaz de Swashbuckle lleva la misma maquinaria (IsGZipAccepted, un GZipStream en modo descompresión para el cliente raro que se niegue). Así que los tamaños de los recursos almacenados son los tamaños de transferencia, y puedes leerlos de los paquetes sin ejecutar nada:

// .NET SDK 10.0.201, run as a file-based app: dotnet run res.cs <dll>
using System.Reflection;

var asm = Assembly.LoadFrom(args[0]);
foreach (var name in asm.GetManifestResourceNames())
{
    using var s = asm.GetManifestResourceStream(name);
    Console.WriteLine($"{s?.Length,10}  {name}");
}

Scalar sirve tres assets, y solo dos de ellos son código:

   1070166  ScalarStaticAssets.scalar.js
      1111  ScalarStaticAssets.scalar.aspnetcore.js
       533  ScalarStaticAssets.favicon.svg

El index.html de Swashbuckle carga el bundle, el preset standalone, la hoja de estilos y su propio inicializador:

    421507  swagger-ui-bundle.js
     77731  swagger-ui-standalone-preset.js
     26499  swagger-ui.css
       433  index.js
       152  index.css
       739  index.html

Eso es 1 071 277 bytes para Scalar frente a 526 322 bytes para Swagger UI, una diferencia de 2.0x en la red. Descomprimido, scalar.js son 3 708 228 bytes de JavaScript que el navegador tiene que parsear, frente a 1 793 552 bytes para el bundle más el preset de Swagger UI. La opción de aspecto moderno es la pesada, que es lo contrario de lo que insinúan la mayoría de los artículos.

Dos advertencias antes de darle demasiado peso a esto. Primero, es una herramienta de desarrollo: los bytes aterrizan en tu máquina, sobre loopback, una vez por carga en frío. Segundo, swagger-ui.js de Swashbuckle (92 466 bytes) queda en el paquete sin usarse en la página por defecto, así que el número de arriba es lo que realmente carga, no lo que se distribuye. Si sirves cualquiera de las dos interfaces sobre una red real, la comparación de compresión de respuestas no te ayuda aquí: ambos paquetes ya comprimieron estos assets por su cuenta, y recomprimir una respuesta con Content-Encoding: gzip no es algo que el middleware vaya a hacer.

El caché es la parte que molesta a diario. SwaggerUIOptions.CacheLifetime documenta su valor por defecto como “0 days (ETags are used to check if resources have been updated)”, así que de fábrica ambas interfaces revalidan. La diferencia es que Swashbuckle te deja optar por caché real y Scalar no: su handler de assets estáticos fija Cache-Control: no-cache en el código y responde a un If-None-Match coincidente con un 304. Pagas un viaje de ida y vuelta por asset por carga de página, para siempre.

// Program.cs -- .NET 11, C# 14, Swashbuckle.AspNetCore.SwaggerUI 10.2.3
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "v1");
    options.CacheLifetime = TimeSpan.FromDays(7); // 304s become cache hits
});

El detalle de .NET 11: tu documento ahora es 3.2

Este es el hecho que debería impulsar la decisión en agosto de 2026, y casi nadie lo ha escrito. Microsoft Learn es explícito: “Starting in .NET 11, the default OpenAPI version for generated documents is 3.2. In .NET 10, the default is 3.1.” Actualiza una API de .NET 10 a .NET 11, sin cambiar nada más, y el documento que tu interfaz tiene que renderizar cambia de versión de especificación.

Del lado de Swagger UI, swagger-ui 5.32.0 (27 de febrero de 2026) incorporó “basic OpenAPI 3.2.0 support”, y Swashbuckle 10.2.3 empaqueta 5.32.7, así que el renderizador al menos sabe qué está mirando. Del lado de Scalar, @scalar/openapi-parser entiende 3.2, pero el issue de seguimiento scalar/scalar#6715 sigue abierto, con “set OpenAPI 3.2 as the default version” y el renderizado de etiquetas profundamente anidadas en la barra lateral listados como trabajo pendiente en su última actualización del 30 de junio de 2026.

En la práctica un documento generado a partir de endpoints de minimal API cambia muy poco entre 3.1 y 3.2, así que la mayoría de las aplicaciones no verán ninguna diferencia. Si ves una barra lateral que agrupa mal o un esquema que se renderiza vacío, fija la versión en lugar de abrir un bug contra la interfaz:

// Program.cs -- .NET 11, C# 14
builder.Services.AddOpenApi(options =>
{
    // .NET 11 defaults to OpenApi3_2; pin 3.1 while a renderer catches up
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});

La misma palanca existe para la generación en tiempo de compilación mediante la propiedad MSBuild OpenApiGenerateDocumentsOptions con --openapi-version OpenApi3_1. Fijarla no te cuesta nada hoy: todavía nada en un documento generado por ASP.NET Core depende de características de 3.2.

Middleware o endpoint, ahora en ambos lados

El argumento arquitectónico más fuerte a favor de Scalar solía ser que MapScalarApiReference registra un endpoint mientras que UseSwaggerUI registra middleware, y el middleware termina la solicitud antes de que el enrutamiento por endpoints tenga algo que decir. Ese argumento expiró en mayo de 2026. Swashbuckle 10.2.0 agregó MapSwaggerUI y MapReDoc “to support endpoint routing”. Ambas interfaces ahora pueden llevar metadatos de endpoint, aparecer en EndpointDataSource y aceptar convenciones de enrutamiento directamente:

// Program.cs -- .NET 11, C# 14
// Scalar: MapScalarApiReference returns an IEndpointConventionBuilder
app.MapScalarApiReference()
   .RequireAuthorization("ApiDocsPolicy");

// Swashbuckle 10.2.0+: same shape
app.MapSwaggerUI()
   .RequireAuthorization("ApiDocsPolicy");

Si estás detrás de un proxy inverso, ten en cuenta que el endpoint HTML de Scalar redirige una solicitud a /scalar hacia /scalar/ con un 301 para que sus rutas relativas de assets resuelvan, y el middleware de Swashbuckle hace un 301 de una solicitud a su prefijo de ruta desnudo hacia index.html. Una prueba de integración que afirme un 200 en la ruta desnuda falla contra cualquiera de los dos.

Authorize, y qué pasa después de hacer clic

Ambas interfaces leen los esquemas de seguridad del documento, y ninguna los inventa. La propia documentación de Scalar es tajante: tu documento OpenAPI ya debe incluir los esquemas para que Scalar pueda trabajar con ellos. Si no los pusiste ahí, el recorrido por los transformadores de operación y esquema es el mecanismo que necesitas.

Lo que difiere es la ergonomía a partir de ahí. Scalar rellena previamente las credenciales desde la configuración del servidor y puede persistirlas entre recargas:

// Program.cs -- .NET 11, C# 14, Scalar.AspNetCore 2.16.20
app.MapScalarApiReference(options =>
{
    options.AddPreferredSecuritySchemes("Bearer")
           .AddHttpAuthentication("Bearer", auth => auth.WithToken(devToken));
    options.PersistentAuthentication = true;
});

El equivalente de Swagger UI vive en el objeto de configuración y, para OAuth2, en la página oauth2-redirect.html que Swashbuckle incrusta por ti (664 bytes de script de redirección que llevan una década en circulación):

// Program.cs -- .NET 11, C# 14, Swashbuckle.AspNetCore.SwaggerUI 10.2.3
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "v1");
    options.OAuthClientId("dev-client");
    options.OAuthUsePkce();
    options.EnablePersistAuthorization();
});

La única capacidad que Scalar tiene y Swagger UI no es proxyUrl. El Try It de Swagger UI dispara un fetch desde el origen de la documentación, así que una API entre orígenes sin CORS permisivo produce un error de navegador que parece un fallo del servidor. Scalar puede enrutar la solicitud a través de un proxy en su lugar. Si tu documentación se aloja aparte de la API, esa única opción lo decide.

Los ejemplos de código son la diferencia real de producto

Swagger UI te muestra el comando curl de la solicitud que acabas de ejecutar. Scalar renderiza la solicitud en cada cliente que conoce antes de que envíes nada: shell (curl, httpie), JavaScript (fetch, axios, jquery), Node, Python, Go, Java, Ruby, PHP y más, controlado por hiddenClients y defaultHttpClient. Para una API interna donde quienes leen son las mismas personas que la escribieron, eso es decoración. Para una API pública donde quien lee está decidiendo si tu producto es fácil de integrar, es la página entera.

Scalar además te da searchHotKey (CMD/CTRL+K por defecto), doce temas integrados, customCss y un hook /scalar/config.js para configuración arbitraria del cliente. La personalización de Swagger UI pasa por InjectStylesheet, InjectJavascript y el sistema de plugins de swagger-ui, que es más potente y mucho menos agradable, y ese es el resumen honesto de toda la comparación.

Cuándo elegir cada uno

Elige Scalar cuando la documentación sea una superficie de producto, cuando quienes leen estén fuera de tu equipo, cuando quieras el constructor de solicitudes y los ejemplos de código, o cuando la documentación esté alojada en un origen distinto al de la API y necesites el proxy.

Elige Swagger UI cuando quieras el payload más pequeño y un max-age real, cuando tengas una configuración OAuth2 existente que ya funciona, cuando alguien del equipo dependa de un plugin de swagger-ui, o cuando quieras el renderizador con soporte explícito de 3.2 mientras .NET 11 emite 3.2 por defecto.

No elijas ninguno, y usa Swashbuckle.AspNetCore.ReDoc o una extensión del editor, cuando el documento lo consuman clientes generados en lugar de personas. No hay ninguna regla que diga que una API necesita una referencia renderizada.

Elijas lo que elijas, Microsoft Learn expone la postura de seguridad con claridad: las interfaces de usuario de OpenAPI solo deberían habilitarse en entornos de desarrollo. Ambos paquetes convierten eso en una guarda de entorno de una línea, y la versión paso a paso de esa configuración, incluyendo el bloqueo en producción y los assets offline, está en el recorrido de Scalar.

Los detalles que deciden por ti

Entre dos renderizadores con licencia MIT del mismo documento, esta es una decisión reversible: cambiar una línea de Program.cs y una referencia de paquete te mueve en cualquier dirección en unos cinco minutos. Elige según quien lee, no según el framework.

Relacionado

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver