Blazor WebAssembly sigue lento en .NET 5 y no será usable hasta que se implemente AOT ¿en .NET 6.0?

Blazor es la evolución lógica del stack de Microsoft para desarrollar aplicaciones webs y un excelente candidato para aquellos equipos de desarrollo que solo programen en C# y .NET. Blazor WebAssembly es el modelo de hospedaje de blazor que permite que la aplicación ejecute directamente en el navegador, de manera similar a las aplicaciones construidas con Vue, ReactJS o Angular.

Por otro lado, poder ejecutar código .NET en el navegador es tentador, sobre todo, si se obtuviese el rendimiento nativo que promete WebAssembly (WASM).

Figura 1 - Modelo de ejecución de WebAssembly

Sin embargo, a día de hoy (con la release de .NET 5), Blazor WebAssembly dista mucho de ser eficiente y productivo, y a continuación veremos por qué.

Blazor WebAssembly no compila a WebAssembly

La versión actual de Blazor WebAssembly (5.x), a diferencia de lo que pudiera pensarse, no compila nuestros ensamblados .NET (IL) a WASM, sino que los interpreta en el navegador (solo el runtime de .NET utilizado está escrito en WebAssembly). Y esta interpretación acarrea una penalización de rendimiento considerable (Y Microsoft lo sabe). De hecho, puede llegar a ser en ocasiones 50 veces más lento que el código JavaScript de toda la vida.

La funcionalidad de compliar nuestros ensamblados a WASM se conoce como Ahead Of Time (AOT) y está recogida en este issue de GitHub: https://github.com/dotnet/aspnetcore/issues/5466. Esperemos que Microsoft logre introducir AOT para la entrega de .NET 6.

¿Blazor puede llegar a ser 50 veces más lento que JavaScript?

Uno de los atractivos que le veo a Blazor, es la posibilidad de ejecutar algoritmos escritos en .NET directamente en el navegador, y así explotar el “Edge Computing”. Es decir, realizar procesamiento de imágenes, cálculos importantes, o incluso la ejecución de algoritmos de inteligencia artificial. Sin embargo, ahora mismo no es práctico.

Para demostrarlo, basta con implementar el algoritmo Fibonacci que devuelve el n-ésimo término de la sucesión de fibonacci. La implementación que hemos seleccionado es la recursiva, ya que explota CPU y memoria a la vez.

A continuación, una muestra en JavaScript y C# (Blazor), en donde solicitamos fib(33) cinco veces.

JAVASCRIPT:

<script>
    function fib(n)
    {
        if ((n == 1) || (n == 2))
            return 1;
        else
            return fib(n - 1) + fib(n - 2);
    }

    function doFib() {
        const startDateTime = new Date();
        var result = 0;
        for (var i = 0; i < 5; i++)
            result = fib(33);
        const totalMilliseconds = (new Date()-startDateTime);
        alert(totalMilliseconds)
    }
</script>

BLAZOR:

    private int totalMilliseconds = 0;

    public static int Fib(int n)
    {
        if ((n == 1) || (n == 2))
            return 1;
        else
            return Fib(n - 1) + Fib(n - 2);
    }

    private void DoFib()
    {
        var startDateTime = DateTime.Now;
        var result = 0;
        for (var i = 0; i < 5; i++)
            result = Fib(33);
        totalMilliseconds = (int)(DateTime.Now - startDateTime).TotalMilliseconds;
    }

Hemos realizado la ejecución en igualdad de condiciones, es decir, mismo equipo, mismo navegador, mismo sistema operativo (Google Chrome 87, con un CPU Intel Core i7 4Ghz y 64GB RAM).

Prueba Duración
JavaScript 129ms
Blazor en modo Release 1174ms
Blazor con depuración 5154ms

Como puede apreciarse, Blazor en modo release (listo para producción 🤪) es casi 10 veces más lento que el mismo método ejecutado en JavaScript puro. Pero no solo eso, si ejecutas el código con el depurador conectado (sin poner ningún punto de ruptura), se ralentiza considerablemente llegando a ser 50 veces más lento. 😲

Pudiéramos pensar, bueno, solo hago aplicaciones de formularios y reportes, no hay algoritmos complejos así que esto no me afecta… pues aun así, la sensación de lentitud no desaparece. Ya sea en Visual Studio Code o Visual Studio 2019, el tiempo de respuesta, la latencia de las acciones de depuración, y la experiencia en general, es muy lenta. Y esta lentitud te chocará aún más si tienes experiencia desarrollando con ReactJS, en donde la experiencia de desarrollo es fluida y ágil.

¿Realmente es más rápido WebAssembly (WASM) que JavaScript?

Ya hemos visto que Blazor WebAssembly NO compila a WebAssembly, pero existen otras tecnologías que sí lo hacen. Por ejemplo, el lenguage de programación Rust permite compilar directamente a WebAssembly. Siguiendo con nuestra idea de probar Fibonnaci, hemos realizado la implementación en Rust:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fib(n: u32) -> u32 {
   if n == 0 || n == 1 {
      return n;
   }
   
   fib(n - 1) + fib(n - 2)
}

Los resultados, como se podría imaginar, mejoran la ejecución de JavaScript:

Prueba Duración
JavaScript 129ms
Rust WASM 102ms

Y esos tiempos son sin aplicar las optimizaciones del compilador. Es posible mejorar ese tiempo aún más, aunque se sale del alcance de este artículo.

¿Existe algún compilador de .NET a WebAssembly?

Sí, Mono wasm. Mono es la implementación alternativa de .NET realizada por Miguel de Icaza (que potencia Xamarin) y que fue comprada hace unos años por Microsoft 😉. Parte del código de Blazor parte de aquí, pero lamentablemente, Blazor todavía no soporta compilación Full-AOT como mono wasm.

Compilar con Mono wasm actualmente no es trivial, y se requieren, además de un conocimiento elevado, un conjunto de herramientas disponibles solo para Linux (en inglés diríamos que es algo hacky). Sin embargo, hay compañías que lo utilizan en producción con buenos resultados, como es el caso de Plain Concepts y su motor 3D que ejecuta en el navegador.

Nosotros utilizamos esta guía y ejecutamos, una vez más, fibonacci en C#.

Prueba Duración
JavaScript 129ms
Mono WASM Full-AOT 15ms

Ahí tenéis, casi 10 veces más rápido que JavaScript 🥳. Este resultado es esperanzador, sobre todo para Blazor. Solo toca esperar que Microsoft incorpore compilación AOT en Blazor para .NET 6.0 🙏.

Entonces… ¿Utilizo o no Blazor?

Esta es una decisión difícil, y solo podré daros mi opinión personal:

Yo lo he intentado en más de una ocasión, y siempre termino volviendo a TypeScript/ReactJS. Con las versiones preliminares reboté, porque las herramientas de desarrollo estaban en un estado muy inicial y pasaba más tiempo buscando workarounds que desarrollando la aplicación. La versión actual (5.x) está más trabajada pero todavía no se siente pulida, sobre todo, porque se pierde mucho tiempo de desarrollo, a falta del hot reload existente en frameworks similares y la alta latencia que impone la depuración. Hay workarounds no oficiales para emular el hot reload pero no son usables más allá de ejemplos triviales. Microsoft está trabajando en una solución oficial: https://github.com/dotnet/aspnetcore/issues/5456.

¿Será la versión 6.x de Blazor la buena? Pues vamos a ver si algo hace Microsoft con blazor en .NET 6.0 y nos sorprende en este 2021.

Compartir con:

Alejandro Tamayo Castillo

Profesor, Investigador, Arquitecto de software, Consultor y entusiasta de la tecnología. Master of Science (MSc) in Computer Science actualmente trabajando con tecnología full-stack de Microsoft y otros fabricantes, como .NET Core, ASP.NET Core, Azure, Serverless, SPA/ReactJS, HTML5, Xamarin (Android/iOS/Forms), Orchard, SharePoint, HoloLens, Bots, y otros temas chulos.

Seguir a Alejandro Tamayo Castillo en LinkedIn