ASP.NET Bundling: CDN, archivos secuenciales, directorios y Requirejs

Uno de los puntos más preocupantes cuando comienzas a trabajar con Single Page Applications es la cantidad de archivos JavaScript que vamos a manejar. Generalmente, un navegador está diseñado para realizar unas 6 peticiones concurrentes al mismo dominio, por lo que es necesario reducir tanto el número como el tiempo necesario para conseguir cada uno de los recursos solicitados. Normalmente las tres inquietudes principales son las imágenes, archivos JavaScript y estilos CSS. En este post quisiera comentar algunos consejos sobre estos dos últimos 🙂

ASP.NET Bundling

Si bien no es ninguna técnica innovadora, desde la versión 4.5 de ASP.NET podremos disfrutar de un sistema de compresión y reducción (ofuscando, eliminando los blancos y comentarios/documentación) de nuestros archivos gracias a lo que se conoce como bundles tanto en ASP.NET MVC como en Web Forms. En ASP.NET MVC podemos encontrar su inicialización en el archivo Global.asax y su implementación está ubicada en App_Start/BundleConfig.cs. A partir de la plantilla Basic podemos estudiar una serie de bundles básicos para el uso de Modernizr, JQuery, JQuery Validation e incluso los CSS por defecto de la plantilla y de JQuery UI. En este post me gustaría ir un paso más allá y mostrar otras configuraciones de bastante utilidad:

using System.Web;
using System.Web.Optimization;
namespace Bundles
{
    public class BundleConfig
    {
        // For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
        public static void RegisterBundles(BundleCollection bundles)
        {
            //Modernizr at the top
            bundles.Add(new ScriptBundle("~/bundles/modernizr")
                .Include("~/Scripts/lib/modernizr-*"));
            //jQuery + CDN            
            bundles.UseCdn = true;
            bundles.Add(new ScriptBundle("~/bundles/jquery",
                                         "//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js") //CDN
                                         .Include("~/Scripts/lib/jquery-{version}.js")); //Local
            //Sequencial scripts
            bundles.Add(new ScriptBundle("~/bundles/jqueryval")
                .Include(
                "~/Scripts/lib/jquery.unobtrusive*",
                "~/Scripts/lib/jquery.validate*"));
            //Directory
            bundles.Add(new ScriptBundle("~/bundles/jsext")
                .IncludeDirectory("~/Scripts/lib/extensions", //Virtual Path
                                  "*.js", // Search pattern
                                  searchSubdirectories: true)); //Search subdirectories
            //Custom scripts
            bundles.Add(new ScriptBundle("~/bundles/app")
                .IncludeDirectory("~/Scripts/app", "*.js")); //Be careful!
            
            //CSS
            bundles.Add(new StyleBundle("~/Content/css")
                .Include("~/Content/site.css"));
        }
    }
}

¿Cómo funciona? Básicamente BundleConfig.cs recibe una colección de bundles donde existen tres tipos: ScriptBundle para los scripts, StyleBundle para las hojas de estilos y Bundle para aquellos casos que queramos personalizar la unificación, interpretación y reducción de los archivos elegidos para ese tipo.
La forma de utilizarlo en nuestras páginas (normalmente en el layout) es haciendo uso de los helpers @Scripts.Render y @Styles.Render como podemos ver en las plantillas de ASP.NET MVC.
Creamos la instancia que necesitemos donde, como mínimo, debemos indicar el nombre del bundle y la ruta de cada uno de los archivos que deseamos unir bajo el mismo. Como vemos en los ejemplo anteriores, podemos hacer uso del * para generalizar ciertas partes de la ruta como la extensión, nombres de los archivos, etcétera (interesante ya que ASP.NET Bundling reconoce los archivos de tipo .min.js) e incluso podemos utilizar {version} para desligar el número de la versión del archivo (Realmente útil cuando actualizamos la versiones, ya que la configuración de los bundles no se vería afectada).
Cuando se trabaja en local el efecto del bundling sólo es el de unificar bajo un mismo nombre la inyección de varios archivos en la página, por lo que seremos capaces de depurar y mantener nuestro código formateado de forma legible. Para comprobar la generación de un único archivo y la reducción del contenido de cada uno de los recursos que compone cada bundle basta con modificar a false el modo debug en el Web.config:

    <compilation debug="false" targetFramework="4.5"/>

Gracias a las transformaciones de los archivos de configuración seremos capaces de modificar este valor de manera automática cuando sea necesaria la publicación del sitio web, siempre y cuando establezcamos la misma en el modo Release.

Ahora veamos cada caso por separado:

Modernizr

            //Modernizr at the top
            bundles.Add(new ScriptBundle("~/bundles/modernizr")
                .Include("~/Scripts/lib/modernizr-*"));

En primer lugar es importante recordar que, incluso con el sistema de Bundling, Modernizr debe permanecer en la cabecera aunque las buenas prácticas de optimización web, donde nos cuentan que los scripts deben estar situados en la parte inferior del body, así lo indiquen. El motivo es que Modernizr habilitará los elementos HTML 5 que versiones antiguas de Internet Explorer no reconoce y debe ser ejecutado antes que la etiqueta body. Por otro lado si se utilizan cualquiera de las clases CSS que son añadidas por Modernizr también se recomienda esta ubicación para evitar lo que se conoce como FOUC: Flash of Unstyled Content.

JQuery y CDN

            //jQuery + CDN            
            bundles.UseCdn = true;
            bundles.Add(new ScriptBundle("~/bundles/jquery",
                                         "//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js") //CDN
                                         .Include("~/Scripts/lib/jquery-{version}.js")); //Local

En el siguiente ejemplo estamos definiendo otro bundle exclusivo para JQuery, ya que normalmente hay otras librerías de terceros (e incluso las nuestras propias) que necesitan de este potente framework para funcionar correctamente. En él además queremos hacer uso de un CDN (Content Delivery Network) para que, en el caso de ser posible, poder hacer uso de otro lugar para descargar este recurso, con lo que obtendríamos mejor rendimiento en la descarga liberando a nuestro servidor de esta petición. Indicamos el nombre del bundle, la dirección del CDN y el archivo local que es equivalente al mismo en tiempo de depuración.

Un punto preocupante es ¿Qué ocurre si el archivo que hemos indicado en otro dominio deja de estar disponible? En este caso es importante disponer de un fallback y evitar de este modo errores inesperados. Para ello podemos añadir la siguiente línea en nuestro layout:

    @Scripts.Render("~/bundles/jquery")
    <!--JQuery Fallback-->
    <script>window.jQuery || document.write('<script src="/Scripts/lib/jquery-1.9.1.min.js">x3C/script>')</script>

Si el archivo ubicado en el CDN no está disponible o no ha sido posible descargarlo, se hará acto seguido una comprobación para conocer si window.JQuery está disponible y saber si la descarga ha sido satisfactoria. De no ser así, se inyectará en ese punto el script alojado en local que indiquemos.

Archivos secuenciales

            //Sequencial scripts
            bundles.Add(new ScriptBundle("~/bundles/jqueryval")
                .Include(
                "~/Scripts/lib/jquery.unobtrusive*",
                "~/Scripts/lib/jquery.validate*"));

Otra posible opción es indicar más de un archivo dentro de un mismo bundle. Para ello, dentro del apartado Include añadiremos cada uno de los recursos separados con una coma. De este modo conoceremos el orden en el cual se cargará cada uno de los archivos.

Directorio

            //Directory
            bundles.Add(new ScriptBundle("~/bundles/jsext")
                .IncludeDirectory("~/Scripts/lib/extensions", //Virtual Path
                                  "*.js", // Search pattern
                                  searchSubdirectories: true)); //Search subdirectories

También es posible incluir un directorio entero bajo un bundle. Para ello definimos el nombre del bundle, la ruta de nuestro directorio y un patrón para saber qué tipo de archivos queremos incluir. Como parámetro opcional podemos decir además si queremos que la búsqueda también se haga en posibles subdirectorios que pudieran existir dentro del indicado.

Archivos javascript de la aplicación

            //Custom scripts
            bundles.Add(new ScriptBundle("~/bundles/app")
                .IncludeDirectory("~/Scripts/app", "*.js")); //Be careful!

Cuando queremos utilizar bundles para el código JavaScript creado propiamente para la aplicación debemos prestar especial atención ya que es posible que tengan relación entre ellos y si definimos el bundle en modo directorio podemos perder el orden correcto de la carga y que nuestro desarrollo deje de funcionar en producción. Existen algunas alternativas para evitar este escenario:

  1. A través del nombre del archivo, podríamos enumerar los scripts dentro del directorio en el orden que nos gustaría que se cargaran.
  2. Hacer uso de RequireJS para conseguir la definición de dependencias y que el orden de carga no sea un problema.

Bundles y Requirejs

Si optamos por trabajar con Requirejs es importante saber que es totalmente compatible con ASP.NET Bundling. El único requisito es que el archivo de requirejs sea cargado antes que los módulos y el resto de archivos pueden ser incluidos en un bundle como hemos visto anteriormente (incluso a través de la búsqueda por directorio :D). En el layout de la página podemos cargar requirejs y el bundle de la siguiente forma:

    @Scripts.Render("~/Scripts/lib/require.js",
                    "~/bundles/app")

Hojas de estilo

            //CSS
            bundles.Add(new StyleBundle("~/Content/css")
                .Include("~/Content/site.css"));

En cuanto a los archivos CSS se refiere, el sistema es exactamente el mismo que para los scripts. La única diferencia es el tipo de objeto a instanciar: StyleBundle. También podemos hacer uso del asterisco, {version} e incluso indicar un directorio en lugar de los archivos específicos. También es importante tener especial cuidado para evitar la mala aplicación de los estilos por un orden incorrecto. Si quisiéramos hacer uso de la búsqueda a través de directorio una opción sería enumerar los archivos para su correcta inyección en la página.

Espero que haya sido de utilidad.

¡Saludos!