Bundling in MVC




In most of the browsers simultaneous connections for each host name is limited to 6. That means that while six requests are being processed, additional requests on a host will be queued by the browser.

B/M

Bars indication:


1. The gray bars show the time the request is queued by the browser waiting on the six connection limit
2. The yellow bar is the request time to first byte, that is the time taken to send the request and receive the first response from the server. 
3. The blue bars show the time taken to receive the response data from the server. 


Double-click to get detailed timing information.



Bundling in detail:
Bundling introduced in ASP.NET 4.5.
Bundle creates a single file for one or more files.
Bundling can be done for .css and .js files.
You can create CSS, JavaScript and other bundles. 

Advantage: Reduces multiple request of the files to single bundled file which improves page load  performance.
Below are the timing deference after enabling bundling.

Impact of Bundling:

The following table shows several important differences between listing all the assets individually and using bundling and minification (B/M) in the sample program.
Using B/MWithout B/MChange
File Requests934256%
KB Sent3.2611.92266%
KB Received388.5153036%
Load Time510 MS780 MS53%
The bytes sent had a significant reduction with bundling as browsers are fairly verbose with the HTTP headers they apply on requests. The received bytes reduction is not as large because the largest files (Scripts\jquery-ui-1.8.11.min.js and Scripts\jquery-1.7.1.min.js) are already minified

Debugging Bundled Application:

To debug your JavaScript check the compilation Element in the Web.config file is set to debug="true"  which stops the JavaScript files to be bundled or debug a release build where your JavaScript files are bundled. Using the IE F12 developer tools, you can debug using the following approach:
  1. Script tab ->Start debugging
  2. Select the bundle containing the JavaScript function you want to debug.
  3. Format if minified JavaScript by selecting the Configuration button -> Format JavaScript
  4. In the Search Script input box, select the name of the function you want to debug. In the following image, AddAltToImg was entered in the Search Script input box.

Using Bundling in ASP.NET MVC

Enabling/Disabling bundling in MVC Application:

In App_Start\BundleConfig.cs file.

To disable bundling set debug=true in the Web.Config file and BundleTable.EnableOptimizations = false.

<system.web>
    <compilation debug="true" />
    <!-- Lines removed for clarity. -->
</system.web>
RegisterBundles function can be used to register all the bundles as below:
public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                 "~/Scripts/jquery-{version}.js"));

    // Code removed for clarity.
    BundleTable.EnableOptimizations = true;
}
Bundling in detail:

The preceding code creates a new JavaScript bundle named "~/bundles/jquery" that includes all the appropriate (that is debug or minified but not .vsdoc) files in the Scripts folder that match the wild card string "~/Scripts/jquery-{version}.js". For ASP.NET MVC 4, this means with a  debug configuration, the file jquery-1.7.1.js will be added to the bundle. In a release configuration,  jquery-1.7.1.min.js will be added. The bundling framework follows several common conventions such as:
  • Selecting “.min” file for release when “FileX.min.js” and “FileX.js” both files exists on same location.
  • Selecting the non “.min” version for debug.
  • Ignoring “-vsdoc” files (such as jquery-1.7.1-vsdoc.js), which are used only by IntelliSense.
The {version} wild card matching shown above is used to automatically create a jQuery bundle with the appropriate  version of jQuery in your Scripts folder.  In this example, using a wild card provides the following benefits:
  • Allows you to use NuGet to update to a newer jQuery version without changing the preceding bundling code or jQuery references in your view pages.
  • Automatically selects the full version for debug configurations and the ".min" version for release builds.

Using a CDN

To use CDN replace the local file path to CDN file path and add bundles.UseCdn = true;
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 = "http://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.
}
In above code file will be requested from the CDN in release mode  and  file from local path in debug mode. When using a CDN, you should have a fallback mechanism in case the CDN request fails. The following markup fragment from the end of the layout file shows script added to request script file should the CDN fail.
        @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)
    

Creating a Bundle:

The Bundle class Include method takes an array of strings, where each string is a virtual path to resource. The following code from the RegisterBundles method in the App_Start\BundleConfig.cs file shows how multiple files are added to a bundle:
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"));
The Bundle class IncludeDirectory method is provided to add all the files in a directory (and optionally all sub directories) which match a search pattern. 
 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.
Bundles are referenced in views using the Render method , (Styles.Render for CSS and Scripts.Render for JavaScript). The following markup from the Views\Shared\_Layout.cshtml file shows how the default ASP.NET internet project views reference CSS and JavaScript bundles.
<!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>
Notice in Render methods  you can add multiple bundles in one line . You can use the Url method to generate the URL to the asset without the markup needed to reference the asset. Suppose you wanted to use the new HTML5 async attribute. The following code shows how to reference modernizr using the Url method.
<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>

Using the "*" wildcard character to select files:

The virtual path specified in the Include method  and  the search pattern in the IncludeDirectory method can accept one "*" wildcard character as a  prefix or suffix to the last segment of the path. The search string is case insensitive. The IncludeDirectory method has the option of searching sub-directories.
 Consider a project with the following JavaScript files:
  • Scripts\Common\AddAltToImg.js
  • Scripts\Common\ToggleDiv.js
  • Scripts\Common\ToggleImg.js
  • Scripts\Common\Sub1\ToggleLinks.js
dir imag
The following table shows the files added to a bundle using the wildcard as shown:
CallFiles Added or Exception Raised
Include("~/Scripts/Common/*.js")AddAltToImg.js, ToggleDiv.js, ToggleImg.js
Include("~/Scripts/Common/T*.js")Invalid pattern exception. The wildcard character is only allowed on the prefix or suffix.
Include("~/Scripts/Common/*og.*")Invalid pattern exception. Only one wildcard character is allowed.
"Include("~/Scripts/Common/T*")ToggleDiv.js, ToggleImg.js
"Include("~/Scripts/Common/*")Invalid pattern exception. A pure wildcard segment is not valid.
IncludeDirectory("~/Scripts/Common", "T*")ToggleDiv.js, ToggleImg.js
IncludeDirectory("~/Scripts/Common", "T*",true)ToggleDiv.js, ToggleImg.js, ToggleLinks.js
Explicitly adding each file to a bundle  is generally the preferred over wildcard loading of files for the following reasons:
  • Adding scripts by wildcard defaults to loading them in alphabetical order, which is typically not what you want. CSS and JavaScript files frequently need to be added in a specific (non-alphabetic) order. You can mitigate this risk by adding a custom IBundleOrderer implementation, but explicitly adding each file is less error prone. For example, you might add new assets to a folder in the future which might require you to modify your IBundleOrderer implementation.
  • View specific files added to a directory using wild card loading can be included in all views referencing that bundle. If the view specific script is added to a bundle, you may get a JavaScript error on other views that reference the bundle.
  • CSS files that import other files result in the imported files loaded twice. For example, the following code creates a bundle with most of the jQuery UI theme CSS files loaded twice.
    bundles.Add(new StyleBundle("~/jQueryUI/themes/baseAll")
        .IncludeDirectory("~/Content/themes/base", "*.css"));
    The wild card selector "*.css" brings in each CSS file in the folder, including  the Content\themes\base\jquery.ui.all.css file.  The jquery.ui.all.css file imports other CSS files.

Bundle Caching:

Bundles set the HTTP Expires Header one year from when the bundle is  created. If you navigate to a previously viewed page, Fiddler shows IE does not make a conditional request for the bundle, that is, there are no HTTP GET requests from IE for the bundles and no HTTP 304 responses from the server. You can force IE to make a conditional request for each bundle with the F5 key (resulting in a HTTP 304 response for each bundle). You can  force a full refresh by using Ctrl+F5 (resulting in a HTTP 200 response for each bundle.)
The following image shows the Caching tab of the Fiddler response pane:
fiddler caching image
The request
http://localhost/MvcBM_time/bundles/AllMyScripts?v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81
is for the bundle AllMyScripts and contains a query string pair v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81. The query string v has a value token that is a unique identifier used for caching. As long as the bundle doesn't change, the ASP.NET application will request the AllMyScripts  bundle using this token. If any file in the bundle changes, the ASP.NET optimization framework will generate a new token, guaranteeing that browser requests for the bundle will get the latest bundle.
If you run the IE9 F12 developer tools and navigate to a previously loaded page, IE incorrectly shows conditional GET requests made to each bundle and the server returning HTTP 304. You can read why IE9 has problems determining if a conditional request was made  in the blog entry Using CDNs and Expires to Improve Web Site Performance.

LESS, CoffeeScript, SCSS, Sass Bundling:

The bundling and minification framework provides a mechanism to process intermediate languages such as SCSSSassLESS or Coffeescript, and apply transforms such as minification to the resulting bundle. For example, to add .less files to your MVC 4 project:
  1. Create a folder for your LESS content. The following example uses the Content\MyLess folder.
  2. Add the .less NuGet package dotless to your project.
    NuGet dotless install
  3. Add a class that implements the IBundleTransform interface. For the .less transform, add the following code to your project.
    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. Create a bundle of LESS files with the LessTransform and the CssMinify transform. Add the following code to the RegisterBundles method in the App_Start\BundleConfig.cs file.
    var lessBundle = new Bundle("~/My/Less").IncludeDirectory("~/My", "*.less");
    lessBundle.Transforms.Add(new LessTransform());
    lessBundle.Transforms.Add(new CssMinify());
    bundles.Add(lessBundle);
  5. Add the following code to any views which references the LESS bundle.
     @Styles.Render("~/My/Less");

Bundle Considerations:

A good convention to follow when creating bundles is to include "bundle" as a prefix in the bundle name. This will prevent a possible routing conflict.
Once you update one file in a bundle, a new token is generated for the bundle query string parameter and the full bundle must be downloaded the next time a client requests a page containing the bundle. In traditional markup where each asset is listed individually, only the changed file would be downloaded. Assets that change frequently may not be good candidates for bundling.

Behavior of bundling:

If you changes any of the bundle file then a new token is created for that bundle. Now automatically browser will refresh your bundle with that new token (example css?v=new_token).

If you added only comment line in .css file it creates new token but if you add comment in .js file then it ignore it and old token will be used.

Every time you will change .css or .js files will create new token but when you will revert your file to old it will automatically use that old token (example css?v=old_token)

Bundling primarily improve the first page request load time. Once a webpage has been requested, the browser caches the assets (JavaScript, CSS and images) so bundling won’t provide any performance boost when requesting the same page, or pages on the same site requesting the same assets. If you don’t set the expires header correctly on your assets, and you don’t use bundling , the browsers freshness heuristics will mark the assets stale after a few days and the browser will require a validation request for each asset. In this case, bundling  provide a performance increase after the first page request. For details, see the blog Using CDNs and Expires to Improve Web Site Performance.
Limited (suppose 6 simultaneousrequests can be increased by using CDN. In this case 6 simultaneous requests from your site and 6 simultaneous requests from each different CDN can be done at the same time. 
A CDN can also provide common package caching and edge caching advantages.
Best practice to implement the bundling is create bundle page wise.

System.Web.Optimization.DLL is the reference need to be added for System.Web.Optimization namespace for enabling bundling.




Note: In simple term when you want to enable bundling set BundleTable.EnableOptimizations = true.

EnableOptimizations overrides the debug attribute in the compilation Element  in the Web.config file.

Additionally if there are two files on same location suppose one is "Script\abc.min.js" and second is "Script\abc.js" and bundling is enabled then "Script\abc.min.js" file will be used by default whether you have used "Script\abc.js" or not but if bundling is disabled then "Script\abc.js" will be used.



Some extra solutions


Problem 1: Browser cache problem/ User not getting latest changes of resources (CSS/JS)/ User need to clear cache forcefully to get latest changes every time: 


Browsers cache resources based on URLs. When a web page requests a resource (fonts/scripts/css), the browser first checks its cache to see if there is a resource with the matched URL. If yes, then it simply uses the cached copy instead of fetching a new one from server. Hence whenever you change the content of css and js files will not reflect on the browser. For this you need to force the browser for refreshing/reloading by deleting cache of browser.
But bundles automatically takes care of this problem by adding a hash code to each bundle as a query parameter to the URL as shown below. Whenever you change the content of css and js files then a new has code will be generated and rendered to the page automatically. In this way, the browser will see a different url and will fetch the new copy of css and js.
So by implementing proper bundling in application you will not face any above cache related issue.

Request for the resources(js/css) without implementing bundling:

























Request for the resources(js/css) after implementing bundling:


































Note: Change only adding comments in css will be a cause for new has code (URL will be changed) but change only adding comments in js will not impact the has code (URL will remain same as hash code will remain same).


Problem 2: 404 error after implementing bundling (resource not exists on given path or your images path is being changed due to bundling implementation)  


Suppose you want to implement bundling in your working application there is a path problem which is related to images/font path one solution is to change path of every image and font which is used in application. But if you want some another easy and quick fix solution then here is the solution follow the below steps:

Example:

<script src="~/Script/aaa.min.js"></script>
<script src="~/Script/bbb.min.js"></script>
<script src="~/Script/js/ccc.min.js"></script>
<script src="~/Script/js/ddd.min.js"></script>


Step 1: Use the same sequence of files to create page wise bundles of local files
Step 2: Create single bundle for the files which is/are same as per file's folder (last folder in which file exists).

Step 3: Use the bundle name with file's folder complete path followed by ~ sign.


bundles.Add(new ScriptBundle("~/Script/CustomName-aaa-bbb").Include(
          "~/Script/aaa.min.js","~/Script/bbb.min.js"));
bundles.Add(new ScriptBundle("~/Script/js/CustomName-ccc-ddd").Include(
          "~/Script/js/ccc.min.js","~/Script/js/ddd.min.js"));

Step 4: Use path of all bundles in every page with the same sequence was used without bundling 

 @Scripts.Render("~/Script/CustomName-aaa-bbb","~/Script/js/CustomName-ccc-ddd");
By using this all images and font related (404) error will go away.


Problem 3: Bundling stopped working on HTTPS Site (Website is working on development properly as soon as it is deployed on HTTPS(production server) bundling is not working and giving 404 resouce not found error.

OR 

Error: Resource interpreted as Stylesheet but transferred with MIME type text/html

If your application was working on development environment where your application was hosted without HTTPS (secured) site but when you deployed it on production environment with HTTPS (secured) site it stopped working. If you check there are issues related to script (check by pressing F12 in chrome) then try the following solution:


In this case first check your bundling whether it is enabled from bundle.config file or not. If enabled then you need to add below code to your HTTPS secured site's Web.config. Now refresh and check.

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
    <remove name="BundleModule" />
    <add name="BundleModule" type="System.Web.Optimization.BundleModule" />
  </modules>
</system.webServer>

Or you can try as below:

<system.webServer>
  <modules>
    <add name="BundleModule" type="System.Web.Optimization.BundleModule" />
  </modules>
</system.webServer>


Problem 4: 403 Access denied when using Bundling:

Every request in ASP.NET is manged through http handlers(for example, static handler, page handler, ashx handler, etc). There is a special HTTP Module called UrlRoutingModule which matches the routes in global.asax. If a route is match then it will chnage the current http handler using HttpContext.RemapHandler method otherwise normal ASP.NET flow will continue. Similarly System.Web.Optimization insert a BundleModule http module which try to match a binding. If a match is found found then it will select the BundleHandler as http handler using HttpContext.RemapHandler method. Internally System.Web.Optimization will leave a match if HostingEnvironment.VirtualPathProvider.FileExists(path) is true or HostingEnvironment.VirtualPathProvider.DirectoryExists(path) is true.
I am personlly suggest you to use a prefix in bundles path like,  
@Styles.Render("~/bundle/Content/themes/base/css", "~/Content/css")
So that no confliction will occur between bundles and routes. See this controller action,
   public class ContentController : Controller
    {
        //
        // GET: /Content/

        public ActionResult css()
        {
            return View();
        }
    }
Clearly the default route with /content/css path matches the css action but the bundling framework will override the http handler.

To remove again and again logout on a specific link/url in between publishing of any page with status code: 301



protected void Application_BeginRequest(object sender, EventArgs e)
        {
            Response.AddHeader("Cache-Control", "max-age=0,no-cache,no-store,must-revalidate");
            Response.AddHeader("Pragma", "no-cache");
            Response.AddHeader("Expires", "Tue, 01 Jan 1970 00:00:00 GMT");
        

}




No comments:

Post a Comment

Static Keyword in .Net

Static Class: Ø    A static class  cannot  be  instantiated Ø    Only  contains   static members  (property/methods/fields) ...