Unión y minificación

Por Rick Anderson

La agrupación y la minificación son dos técnicas que puede usar en ASP.NET 4.5 para mejorar el tiempo de carga de la solicitud. La agrupación y la minificación mejoran el tiempo de carga al reducir el número de solicitudes al servidor y reducir el tamaño de los recursos solicitados (como CSS y JavaScript).

La mayoría de los exploradores más populares actuales limitan el número de conexiones simultáneas por cada nombre de host a seis. Esto significa que, mientras se procesan seis solicitudes, el explorador pondrá en cola solicitudes adicionales para los recursos de un host. En la imagen siguiente, las pestañas de red de las herramientas de desarrollo de IE F12 muestran el tiempo de los recursos necesarios para la vista About (Acerca de) de una aplicación de ejemplo.

B/M

Las barras grises muestran el tiempo en que el explorador pone en cola la solicitud mientras espera el límite de seis conexiones. La barra amarilla es la hora de solicitud para el primer byte, es decir, el tiempo necesario para enviar la solicitud y recibir la primera respuesta del servidor. Las barras azules muestran el tiempo necesario para recibir los datos de respuesta del servidor. Puede hacer doble clic en un recurso para obtener información detallada de tiempo. Por ejemplo, en la imagen siguiente se muestran los detalles de tiempo para cargar el archivo /Scripts/MyScripts/JavaScript6.js.

Screenshot that shows the A S P dot NET developer tools network tab with asset request URLs on the left column and their timings on the right column.

En la imagen anterior se muestra el evento Start, que da la hora en que se puso en cola la solicitud debido al límite del explorador del número de conexiones simultáneas. En este caso, la solicitud se puso en cola durante 46 milisegundos esperando a que se complete otra solicitud.

Unión

La unión es una característica de ASP.NET 4.5 que facilita la combinación o agrupación de varios archivos en uno solo. Puede crear CSS, JavaScript y otros paquetes. Menos archivos significan menos solicitudes HTTP y una mejora en el rendimiento de carga de la primera página.

En la imagen siguiente se muestra la misma vista de tiempo de la vista About (Acerca de) que se mostró anteriormente, pero esta vez con la agrupación y la minificación habilitadas.

Screenshot that shows an asset's timing details tab on the I E F 12 developer tools. The Start event is highlighted.

Minificación

La minificación realiza una variedad de optimizaciones de código diferentes para scripts o css, como quitar espacios en blanco innecesarios y comentarios y acortar los nombres de variables a un carácter. Considere la siguiente función de JavaScript.

AddAltToImg = function (imageTagAndImageID, imageContext) {
    ///<signature>
    ///<summary> Adds an alt tab to the image
    // </summary>
    //<param name="imgElement" type="String">The image selector.</param>
    //<param name="ContextForImage" type="String">The image context.</param>
    ///</signature>
    var imageElement = $(imageTagAndImageID, imageContext);
    imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}

Después de la minimización, reduce la función a lo siguiente:

AddAltToImg = function (n, t) { var i = $(n, t); i.attr("alt", i.attr("id").replace(/ID/, "")) }

Además de quitar los comentarios y los espacios en blanco innecesarios, se ha cambiado el nombre de los siguientes parámetros y variables de la siguiente manera:

Original Nombre cambiado
imageTagAndImageID n
imageContext t
imageElement i

Impacto de la unión y la minimización

En la tabla siguiente se muestran varias diferencias importantes entre enumerar todos los recursos individualmente y usar la agrupación y la minificación (A/M) en el programa de ejemplo.

Uso de A/M Sin A/M Cambio
Solicitudes de archivos 9 34 256 %
KB enviados 3,26 11,92 266 %
KB recibidos 388,51 530 36 %
Tiempo de carga 510 ms 780 ms 53 %

Los bytes enviados tenían una reducción significativa con la agrupación, ya que los exploradores son bastante detallados con los encabezados HTTP que aplican en las solicitudes. La reducción de bytes recibidos no es tan grande porque los archivos más grandes (Scripts\jquery-ui-1.8.11.min.js y Scripts\jquery-1.7.1.min.js) ya están minificados. Nota: los tiempos del programa de ejemplo usaron la herramienta Fiddler para simular una red lenta. (En el menú Rules (Reglas) de Fiddler, seleccione Performance (Rendimiento) y, a continuación, Simulate Modem Speeds (Simular velocidades de módem).

Depuración agrupada y minificada de JavaScript

Es fácil depurar el JavaScript en un entorno de desarrollo (donde el elemento de compilación del archivo Web.config está establecido en debug="true") porque los archivos de JavaScript no se agrupan ni se minimizan. También puede depurar una compilación de versión en la que se agrupan y minifican los archivos de JavaScript. Con las herramientas de desarrollo de IE F12, depura una función de JavaScript incluida en una agrupación minimizada mediante el siguiente enfoque:

  1. Seleccione la pestaña Script y, a continuación, seleccione el botón Iniciar depuración.
  2. Seleccione la agrupación que contiene la función de JavaScript que quiere depurar mediante el botón Assets (Recursos).
    Screenshot that shows the I E F 12 developer tool's Script tab. The Search Script input box, a bundle, and a Java Script function are highlighted.
  3. Para dar formato al JavaScript minificado, seleccione el botón ConfigurationImage that shows the Configuration button icon. (Configuración) y, a continuación, seleccione Format JavaScript (Formato de JavaScript).
  4. En el cuadro de texto Search Script (Buscar script), seleccione el nombre de la función que quiere depurar. En la imagen siguiente, AddAltToImg se especificó en el cuadro de texto Search Script (Buscar script).
    Screenshot that shows the I E F 12 developer tool's Script tab. The Search Script input box with Add Alt To lmg entered in it is highlighted.

Para obtener más información sobre la depuración con las herramientas de desarrollo F12, consulte el artículo de MSDN Using the F12 Developer Tools to Debug JavaScript Errors (Uso de las herramientas de desarrollo F12 para depurar errores de JavaScript).

Control de la agrupación y la minificación

La agrupación y la minificación se habilitan o deshabilitan estableciendo el valor del atributo de depuración en el elemento de compilación del archivo Web.config. En el siguiente XML, debug se establece en true, por lo que la agrupación y la minificación están deshabilitadas.

<system.web>
    <compilation debug="true" />
    <!-- Lines removed for clarity. -->
</system.web>

Para habilitar la agrupación y la minificación, establezca el valor debug en "false". Puede invalidar el valor Web.config con la propiedad EnableOptimizations en la clase BundleTable. El código siguiente habilita la agrupación y la minificación e invalida cualquier configuración en el archivo Web.config.

public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                 "~/Scripts/jquery-{version}.js"));

    // Code removed for clarity.
    BundleTable.EnableOptimizations = true;
}

Nota:

A menos que EnableOptimizations sea true o el atributo debug del elemento de compilación del archivo Web.config esté establecido en false, los archivos no se agruparán ni se minificarán. Además, no se usará la versión .min de los archivos, se seleccionarán las versiones de depuración completas. EnableOptimizations invalida el atributo debug en el elemento de compilación en el archivo Web.config.

Uso de la agrupación y la minificación con ASP.NET Web Forms y Web Pages

Uso de la agrupación y la minificación con ASP.NET MVC

En esta sección se creará un proyecto de ASP.NET MVC para examinar la agrupación y la minificación. En primer lugar, cree un nuevo proyecto de Internet de ASP.NET MVC denominado MvcBM sin cambiar ninguno de los valores predeterminados.

Abra el archivo App\_Start\BundleConfig.cs y examine el método RegisterBundles que se usa para crear, registrar y configurar agrupaciones. En el ejemplo de código siguiente, se muestra una parte del método RegisterBundles.

public static void RegisterBundles(BundleCollection bundles)
{
     bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                 "~/Scripts/jquery-{version}.js"));
         // Code removed for clarity.
}

El código anterior crea un nuevo paquete de JavaScript denominado ~/bundles/jquery que incluye todos los archivos adecuados (que se depuran o minifican, pero no .vsdoc) en la carpeta Scripts que coinciden con la cadena comodín "~/Scripts/jquery-{version}.js". Para ASP.NET MVC 4, esto significa que con una configuración de depuración, el archivo jquery-1.7.1.js se agregará a la agrupación. En una configuración de versión, se agregará jquery-1.7.1.min.js. El marco de agrupación sigue varias convenciones comunes, como por ejemplo:

  • Seleccione el archivo ".min" para su versión cuando FileX.min.js y FileX.js existan.
  • Selección de la versión que no es ".min" para depurar.
  • Omitir los archivos "-vsdoc" (como jquery-1.7.1-vsdoc.js), que solo usa IntelliSense.

La coincidencia de caracteres comodín {version} mostrada anteriormente se usa para crear automáticamente una agrupación de jQuery con la versión adecuada de jQuery en la carpeta Scripts. En este ejemplo, el uso de un carácter comodín proporciona las siguientes ventajas:

  • Permite usar NuGet para actualizar a una versión más reciente de jQuery sin cambiar el código de agrupación anterior ni las referencias de jQuery en las páginas de vista.
  • Selecciona automáticamente la versión completa para las configuraciones de depuración y la versión ".min" para las compilaciones de versión.

Uso de una red CDN

El código siguiente reemplaza la agrupación jQuery local por un lote de jQuery de red CDN.

public static void RegisterBundles(BundleCollection bundles)
{
    //bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
    //            "~/Scripts/jquery-{version}.js"));

    bundles.UseCdn = true;   //enable CDN support

    //add link to jquery on the CDN
    var jqueryCdnPath = "https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js";

    bundles.Add(new ScriptBundle("~/bundles/jquery",
                jqueryCdnPath).Include(
                "~/Scripts/jquery-{version}.js"));

    // Code removed for clarity.
}

En el código anterior, jQuery se solicitará desde la red CDN mientras está en modo de versión y la versión de depuración de jQuery se capturará localmente en modo de depuración. Al usar una red CDN, debe tener un mecanismo de reserva en caso de que se produzca un error en la solicitud de red CDN. El siguiente fragmento de marcado del final del archivo de diseño muestra el script agregado a la solicitud jQuery si se produce un error en la red CDN.

</footer>

        @Scripts.Render("~/bundles/jquery")

        <script type="text/javascript">
            if (typeof jQuery == 'undefined') {
                var e = document.createElement('script');
                e.src = '@Url.Content("~/Scripts/jquery-1.7.1.js")';
                e.type = 'text/javascript';
                document.getElementsByTagName("head")[0].appendChild(e);

            }
        </script> 

        @RenderSection("scripts", required: false)
    </body>
</html>

Creación de una agrupación

En la clase Bundle, el método Include toma una matriz de cadenas, donde cada cadena es una ruta de acceso virtual al recurso. El código siguiente del método RegisterBundles en el archivo App\_Start\BundleConfig.cs muestra cómo se agregan varios archivos a una agrupación:

bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
    "~/Content/themes/base/jquery.ui.core.css",
    "~/Content/themes/base/jquery.ui.resizable.css",
    "~/Content/themes/base/jquery.ui.selectable.css",
    "~/Content/themes/base/jquery.ui.accordion.css",
    "~/Content/themes/base/jquery.ui.autocomplete.css",
    "~/Content/themes/base/jquery.ui.button.css",
    "~/Content/themes/base/jquery.ui.dialog.css",
    "~/Content/themes/base/jquery.ui.slider.css",
    "~/Content/themes/base/jquery.ui.tabs.css",
    "~/Content/themes/base/jquery.ui.datepicker.css",
    "~/Content/themes/base/jquery.ui.progressbar.css",
    "~/Content/themes/base/jquery.ui.theme.css"));

En la clase Bundle, se proporciona el método IncludeDirectory para agregar todos los archivos de un directorio (y, opcionalmente, todos los subdirectorios) que coinciden con un patrón de búsqueda. La API de clase BundleIncludeDirectory se muestra a continuación:

public Bundle IncludeDirectory(
    string directoryVirtualPath,  // The Virtual Path for the directory.
    string searchPattern)         // The search pattern.

public Bundle IncludeDirectory(
    string directoryVirtualPath,  // The Virtual Path for the directory.
    string searchPattern,         // The search pattern.
    bool searchSubdirectories)    // true to search subdirectories.

Se hace referencia a los conjuntos en vistas mediante el método Render (Styles.Render para CSS y Scripts.Render para JavaScript). El siguiente marcado del archivo Views\Shared\_Layout.cshtml muestra cómo las vistas de proyecto de Internet de ASP.NET predeterminadas referencian a paquetes CSS y JavaScript.

<!DOCTYPE html>
<html lang="en">
<head>
    @* Markup removed for clarity.*@    
    @Styles.Render("~/Content/themes/base/css", "~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    @* Markup removed for clarity.*@
   
   @Scripts.Render("~/bundles/jquery")
   @RenderSection("scripts", required: false)
</body>
</html>

Observe que los métodos Render toman una matriz de cadenas, por lo que puede agregar varias agrupaciones en una línea de código. Por lo general, querrá usar los métodos Render que crean el código HTML necesario para hacer referencia al recurso. Puede usar el método Url para generar la dirección URL al recurso sin el marcado necesario para hacer referencia al recurso. Supongamos que quería usar el nuevo atributo de HTML5 async. En el código siguiente se muestra cómo hacer referencia a modernizr mediante el método Url.

<head>
    @*Markup removed for clarity*@
    <meta charset="utf-8" />
    <title>@ViewBag.Title - MVC 4 B/M</title>
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    @Styles.Render("~/Content/css")

   @* @Scripts.Render("~/bundles/modernizr")*@

    <script src='@Scripts.Url("~/bundles/modernizr")' async> </script>
</head>

Uso del carácter comodín "*" para seleccionar archivos

La ruta de acceso virtual especificada en el método Include y el patrón de búsqueda del método IncludeDirectory pueden aceptar un carácter comodín "*" como prefijo o sufijo en el último segmento de ruta de acceso. La cadena de búsqueda no distingue entre mayúsculas y minúsculas. El método IncludeDirectory tiene la opción de buscar subdirectorios.

Considere un proyecto con los siguientes archivos de JavaScript:

  • Scripts\Common\AddAltToImg.js
  • Scripts\Common\ToggleDiv.js
  • Scripts\Common\ToggleImg.js
  • Scripts\Common\Sub1\ToggleLinks.js

dir imag

En la tabla siguiente se muestran los archivos agregados a una agrupación mediante el carácter comodín como se ha mostrado:

Call Archivos agregados o excepciones generadas
Include("~/Scripts/Common/*.js") AddAltToImg.js, ToggleDiv.js, ToggleImg.js
Include("~/Scripts/Common/T*.js") Excepción de patrón no válida. El carácter comodín solo se permite en el prefijo o sufijo.
Include("~/Scripts/Common/*og.*") Excepción de patrón no válida. Solo se permite un carácter comodín.
Include("~/Scripts/Common/T*") ToggleDiv.js, ToggleImg.js
Include("~/Scripts/Common/*") Excepción de patrón no válida. Un segmento de caracteres comodín puro no es válido.
IncludeDirectory("~/Scripts/Common", "T*") ToggleDiv.js, ToggleImg.js
IncludeDirectory("~/Scripts/Common", "T*", true) ToggleDiv.js, ToggleImg.js, ToggleLinks.js

Agregar explícitamente cada archivo a una agrupación suele ser preferente sobre la carga de caracteres comodín de los archivos por los siguientes motivos:

  • Agregar scripts mediante caracteres comodín hace que por defecto se carguen en orden alfabético, que normalmente no es lo adecuado. Los archivos CSS y JavaScript a menudo deben agregarse en un orden específico (no alfabético). Puede mitigar este riesgo agregando una implementación personalizada de IBundleOrderer, pero agregar explícitamente cada archivo es menos propenso a errores. Por ejemplo, puede agregar nuevos recursos a una carpeta en el futuro, lo que podría requerir que modifique la implementación de IBundleOrderer.

  • La visualización de archivos específicos agregados a un directorio mediante la carga de caracteres comodín se puede incluir en todas las vistas que hacen referencia a esa agrupación. Si el script específico de la vista se agrega a una agrupación, puede que provoque un error de JavaScript en otras vistas que hacen referencia a la agrupación.

  • Los archivos CSS que importan otros archivos dan lugar a que los archivos importados se carguen dos veces. Por ejemplo, el código siguiente crea una agrupación con la mayoría de los archivos CSS del tema de jQuery UI cargados dos veces.

    bundles.Add(new StyleBundle("~/jQueryUI/themes/baseAll")
        .IncludeDirectory("~/Content/themes/base", "*.css"));
    

    El selector de caracteres comodín "*.css" incluye cada archivo CSS de la carpeta, incluido el archivo Content\themes\base\jquery.ui.all.css. El archivo jquery.ui.all.css importa otros archivos CSS.

Almacenamiento en caché de agrupaciones

Las agrupaciones establecen el encabezado HTTP Expires a un año desde el momento en que se crea la agrupación. Si navega a una página vista anteriormente, Fiddler muestra que IE no realiza una solicitud condicional para la agrupación, es decir, no hay solicitudes HTTP GET de IE para las agrupaciones y ninguna respuesta HTTP 304 del servidor. Puede forzar que IE realice una solicitud condicional para cada agrupación con la tecla F5 (lo que da lugar a una respuesta HTTP 304 para cada agrupación). Puede forzar una actualización completa mediante ^F5 (lo que da como resultado una respuesta HTTP 200 para cada agrupación).

En la imagen siguiente se muestra la pestaña Almacenamiento en caché del panel de respuesta de Fiddler:

fiddler caching image

Solicitud
http://localhost/MvcBM_time/bundles/AllMyScripts?v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81
es para la agrupación AllMyScripts y contiene un par de cadenas de consulta v=r0sLDicvP58AIXN\_mc3QdyVvVj5euZNzdsa2N1PKvb81. La cadena de consulta v tiene un token de valor que es un identificador único que se usa para el almacenamiento en caché. Siempre que la agrupación no cambie, la aplicación ASP.NET solicitará la agrupación AllMyScripts mediante este token. Si cambia algún archivo de la agrupación, el marco de optimización de ASP.NET generará un nuevo token, lo que garantiza que las solicitudes del explorador para la agrupación obtendrán la agrupación más reciente.

Si ejecuta las herramientas de desarrollo de IE9 F12 y navega a una página cargada anteriormente, IE muestra incorrectamente las solicitudes GET condicionales realizadas a cada agrupación y el servidor devuelve HTTP 304. Puede leer por qué IE9 tiene problemas para determinar si se realizó una solicitud condicional en la entrada de blog Using CDNs and Expires to Improve Web Site Performance (Usar redes CDN y Expirar para mejorar el rendimiento del sitio web).

Agrupaciones LESS, CoffeeScript, SCSS, Sass.

El marco de agrupación y minificación proporciona un mecanismo para procesar lenguajes intermedios, como SCSS, Sass, LESS o Coffeescript y aplicar transformaciones como la minificación en la agrupación resultante. Por ejemplo, para agregar archivos .less al proyecto de MVC 4:

  1. Cree una carpeta para el contenido LESS. En el ejemplo siguiente se usa la carpeta Content\MyLess.

  2. Agregue el paquete NuGet .lesssin puntos al proyecto.
    NuGet dotless install

  3. Agregue una clase que implemente la interfaz IBundleTransform. Para la transformación .less, agregue el código siguiente al proyecto.

    using System.Web.Optimization;
    
    public class LessTransform : IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse response)
        {
            response.Content = dotless.Core.Less.Parse(response.Content);
            response.ContentType = "text/css";
        }
    }
    
  4. Cree una agrupación de archivos LESS con LessTransform y la transformación CssMinify. Agregue el código siguiente al método RegisterBundles en el archivo App\_Start\BundleConfig.cs.

    var lessBundle = new Bundle("~/My/Less").IncludeDirectory("~/My", "*.less");
    lessBundle.Transforms.Add(new LessTransform());
    lessBundle.Transforms.Add(new CssMinify());
    bundles.Add(lessBundle);
    
  5. Agregue el código siguiente a cualquier vista que haga referencia a la agrupación LESS.

    @Styles.Render("~/My/Less");
    

Consideraciones acerca de las agrupaciones

Una buena convención que debe seguir al crear agrupaciones es incluir "bundle" como prefijo en el nombre del lote. Esto impedirá un posible conflicto de enrutamiento.

Una vez actualizado un archivo de una agrupación, se genera un nuevo token para el parámetro de cadena de consulta de agrupación y la agrupación completa debe descargarse la próxima vez que un cliente solicite una página que contenga la agrupación. En el marcado tradicional en el que cada recurso aparece individualmente, solo se descargaría el archivo cambiado. Los recursos que cambian con frecuencia pueden no ser buenos candidatos para la agrupación.

La unión y la minimización mejoran principalmente el tiempo de carga de la solicitud de la primera página. Una vez solicitada una página web, el explorador almacena en caché los recursos (JavaScript, CSS e imágenes), por lo que la agrupación y la minificación no proporcionarán ningún aumento del rendimiento al solicitar la misma página o páginas en el mismo sitio que solicitan los mismos recursos. Si no establece el encabezado Expires correctamente en los recursos y no usa la agrupación y la minificación, la heurística de actualización de los exploradores marcarán los activos obsoletos después de unos días y el explorador requerirá una solicitud de validación para cada recurso. En este caso, la agrupación y la minificación proporcionan una mejora del rendimiento incluso después de la solicitud de la primera página. Para obtener más información, consulte el blog Uso de CDN y Expires para mejorar el rendimiento del sitio web.

La limitación del explorador de seis conexiones simultáneas por cada nombre de host se puede mitigar mediante una red CDN. Dado que la red CDN tendrá un nombre de host diferente al del sitio de hospedaje, las solicitudes de recursos de la red CDN no contarán con el límite de seis conexiones simultáneas al entorno de hospedaje. Una red CDN también puede proporcionar ventajas comunes de almacenamiento en caché de paquetes y almacenamiento en caché perimetral.

Las páginas que las necesitan deben particionar agrupaciones. Por ejemplo, la plantilla predeterminada de ASP.NET MVC para una aplicación de Internet crea un paquete de validación de jQuery independiente de jQuery. Dado que las vistas predeterminadas creadas no tienen entrada y no publican valores, no incluyen la agrupación de validación.

El espacio de nombres System.Web.Optimization se implementa en System.Web.Optimization.dll. Aprovecha la biblioteca WebGrease (WebGrease.dll) para las funcionalidades de minificación, que a su vez usa Antlr3.Runtime.dll.

Uso Twitter para hacer publicaciones rápidas y compartir vínculos. Mi identificador de Twitter es: @RickAndMSFT

Recursos adicionales

Colaboradores