For years, I've been registering scripts and serving them with the ScriptManager in ASP.NET web forms. In recent years, bundling and minification have been added to my toolbox, and I can use them together with ScriptManager to deliver a complete JavaScript experience. In today's web, delivering JavaScript and CSS with a HttpHandler that ends in AXD isn't a standard practice. In fact, developers would much rather concatenate and minify their scripts at deployment time. With the introduction of the Task Runner Explorer in Visual Studio, Microsoft has provided a tool that enables this workflow.

Default Configuration of ASP.NET Script Manager

By default in a new ASP.NET application, a ScriptManager is configured in the Site.Master file with the following content:

<asp:ScriptManager runat="server" ID="scriptMgr">
  <Scripts>
    <%--Framework Scripts--%>
      <asp:ScriptReference Name="MsAjaxBundle" />
      <asp:ScriptReference Name="WebFormsBundle" />
      <asp:ScriptReference Name="jquery" />
      <asp:ScriptReference Name="bootstrap" />
      <asp:ScriptReference Name="respond" />
      <asp:ScriptReference Name="WebForms.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebForms.js" />
      <asp:ScriptReference Name="WebUIValidation.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebUIValidation.js" />
      <asp:ScriptReference Name="MenuStandards.js" Assembly="System.Web" Path="~/Scripts/WebForms/MenuStandards.js" />
      <asp:ScriptReference Name="GridView.js" Assembly="System.Web" Path="~/Scripts/WebForms/GridView.js" />
      <asp:ScriptReference Name="DetailsView.js" Assembly="System.Web" Path="~/Scripts/WebForms/DetailsView.js" />
      <asp:ScriptReference Name="TreeView.js" Assembly="System.Web" Path="~/Scripts/WebForms/TreeView.js" />
      <asp:ScriptReference Name="WebParts.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebParts.js" />
      <asp:ScriptReference Name="Focus.js" Assembly="System.Web" Path="~/Scripts/WebForms/Focus.js" />
    <%--Site Scripts--%>
  </Scripts>
</asp:ScriptManager>

These scripts are rendered into HTML by the ASP.NET runtime as the following:

<script src="/bundles/MsAjaxJs?v=c42ygB2U07n37m_Sfa8ZbLGVu4Rr2gsBo7MvUEnJeZ81" type="text/javascript"></script>
<script src="/bundles/WebFormsJs?v=AAyiAYwMfvmwjNSBfIMrBAqfU5exDukMVhrRuZ-PDU01" type="text/javascript"></script>
<script src="Scripts/jquery-1.10.2.min.js" type="text/javascript"></script>
<script src="Scripts/bootstrap.min.js" type="text/javascript"></script>
<script src="Scripts/respond.min.js" type="text/javascript"></script>

They are delivered at the sizes listed below, for a total of five files downloaded and 191kb:

origFiles

Improving on the Default

At the top of the ScriptManager, there are references to bundled scripts that are defined in App_Start/BundleConfig.cs and hiding in other DLLs installed with our project. The WebFormsBundle and the individual script files defined are actually duplicates to ensure that appropriate scripts are loaded for your page, and the ScriptManager de-dupes the collection at render time.

However, the jQuery, Bootstrap, and Respond scripts are not combined and are served as three additional downloads. The first time I ran into this, I wondered: why are those not optimized? When my site is running in Release mode, the ScriptManager will server the .min.js versions of these files, but I never need to debug into them. Why not just always combine them?

You can force the ScriptManager to combine them by adding a element inside of our ScriptManager and moving those references into it:

<CompositeScript>
    <Scripts>
        <asp:ScriptReference Name="jquery" />
        <asp:ScriptReference Name="bootstrap" />
        <asp:ScriptReference Name="respond" />
    </Scripts>
</CompositeScript>

With that change, my scripts rendered by the ScriptManager in HTML look like this:

<script src="/ScriptResource.axd?d=I6DC3-yada__yada__yada__nothing-important-to-see-here" type="text/javascript"></script>
<script src="/bundles/MsAjaxJs?v=c42ygB2U07n37m_does_this" type="text/javascript"></script>
<script src="/bundles/WebFormsJs?v=AAyiAYwM_code_mean_anything" type="text/javascript"></script>

Alright! Now we're talking... I can define similar composite script elements in ScriptManagerProxy controls on my child pages and those files will be bundled and minified for me. The resultant files downloaded with the following sizes:

scriptManagerMinify

That's now only three files for a total of 107.4kb. Also note: the ScriptManager gzipped that ScriptResource.axd payload that was delivered. No need for our web server to take care of that.

Hands On - Taking Control with Grunt and Uglify

I'm a Do-It-Yourself kinda guy, and I'm not too fond of ASP.NET having to handle the packaging and minifying of these files. I would rather have static files available that my web server can compress, cache, and deliver with long expiration times. The modern web way to do this is to invoke some task runner to process these files as part of a build process.

Microsoft's new Task Runner Explorer facilitates this process in our project, and we can insert a link to a static file for both JavaScript and CSS. If you haven't used tools like Grunt or Node before, follow these steps to install the prerequisite tools you need to start this new workflow:

  1. Install NodeJS from NodeJs.org
  2. Install the Grunt command-line-interface (CLI) with this command from the command prompt:

     npm install -g grunt-cli
    
  3. Create a package.json file in the root of your web application with the following contents:

    {
        "name": "<My Project Name>",
        "version": "0.0.0",   
        "dependencies": {},
        "devDependencies": {
            "grunt": "~0.4.2",
            "grunt-contrib-concat": "~0.3.0",
            "grunt-contrib-uglify": "~0.2.7",
            "grunt-contrib-cssmin": "0.10.0"
        }
    }

    This file will define the libraries necessary to "build" the JavaScript and CSS for our project:

    • grunt-contrib-concat will concatenate files together.
    • grunt-contrib-uglify will minify JavaScript files, stripping all comments, spaces, and renaming variables to very short strings.
    • grunt-contrib-cssmin will minify CSS files using a similar technique to the uglify process
  4. Build a Gruntfile.js on the root of the web project with an initial content of the following:

    module.exports = function (grunt) {
        'use strict';
        // Project configuration
        grunt.initConfig({
            // Metadata
            pkg: grunt.file.readJSON('package.json'),
            banner: '/*! &lt%= pkg.name %> - v&lt;%= pkg.version %> - ' +
                '&lt;%= grunt.template.today("yyyy-mm-dd") %> */\n'
        });
    
        // These plugins provide necessary tasks
        grunt.loadNpmTasks('grunt-contrib-concat');
        grunt.loadNpmTasks('grunt-contrib-uglify');
        grunt.loadNpmTasks('grunt-contrib-cssmin');
    
        // Default task
        grunt.registerTask('default', ['concat', 'uglify', 'cssmin']); 
    };
  5. Customize the Gruntfile to meet your project's needs.

Breaking Down the Gruntfile

That's a lot to get started. Lets break down the content of that Gruntfile.js:

First, the Grunt object configuration is intialized with the initConfig function. Inside of that, some metadata around the package is loaded into a pkg variable and a banner is created with some text formatting that looks eerily similar to ASP.NET tags. Next, tasks are loaded for concat, uglify, and cssmin. Finally, a task called default is registered that will call concat, uglify, and then cssmin in that order.

To complete the Gruntfile, we need to define how the concat, uglify, and cssmin tasks will operate. After the banner definition, lets define the concat task:

banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
    '<%= grunt.template.today("yyyy-mm-dd") %> */\n',
// Task configuration
concat: {
  options: {
    stripBanners: true
  },
  dist: {
    src: ['Scripts/jquery-*.js', '!Scripts/jquery-*.min.*', '!Scripts/jquery-*.intellisense.*', 'Scripts/bootstrap.js', 'Scripts/respond.js', 'js/**/*.js'],
    dest: 'dist/app.js'
  },
  distCss: {
    src: ['Content/bootstrap.css', 'Content/site.css'],
    dest: 'dist/app.css'
  }
},

Inside of this task, the options are set to remove any banners in the files I am concatenating. I then define to subtasks: dist and distCss. The dist subtask will collect any files that match the array of file filters passed in, excluding those files matching filters that start with an exclamation point (!). Those files will be merged and placed in dist/app.js Same thing with the CSS files, placing them in dist/app.css

Next, to "uglify" the JavaScript files, I need to add a task after concat. The syntax for that looks like the following:

uglify: {
  options: {
    banner: '<%= banner %>',
    compress: {
      drop_console: true
    }
  },
  dist: {
    src: ['<%= concat.dist.dest %>'],
    dest: 'dist/app.min.js'
  }
},

This block is very easy to read. It has options defined to add my banner to the top of the file and drop any calls to the console contained within the script being analyzed. There is one subtask called dist that will analyze the file that was written to the dest location by the concat task's dist step. The result will be written to dist/app.min.js

The last task is to configure is the cssmin task. This looks very similar to the uglify task, except I did not provide any options for this one:

cssmin: {
  dist: {
    src: ['<%= concat.distCss.dest %>'],
    dest: 'dist/app.min.css'
  }
}

Just like the uglify step, it will analyze the output of the concat tasks's distCss step and write the output to the dist/app.min.css file.

Running Grunt Automatically

Next is the fun part. I opened the Task Runner Explorer under "View -> Other Windows -> Task Runner Explorer" and see this:

taskRunnerExplorer

I can double-click on a task on the left and it will run the task. What I really want to do is to run the default tasks after a build completes. To enable that, right-click on the default task in the left panel and select 'Bindings -> After Build'. This will instruct Visual Studio to run the Grunt task after any project builds.

Connecting the dots

Finally, I added my new files to the default master page so that they are used everywhere:

<link rel="stylesheet" type="text/css" href="<%= ResolveUrl("~/dist/app.min.css") %>" />
...
<asp:ScriptManager runat="server" ID="scriptMgr">
  <Scripts>
    ...  
    <%--Site Scripts--%>
    <asp:ScriptReference Path="~/dist/app.min.js" ScriptMode="Release" />
  </Scripts>
</asp:ScriptManager>

I marked the app.min.js file as Release mode so that the ScriptManager doesn't try to derive any filenames that don't exist.

Summary

In this article I showed how to add the JavaScript tool Grunt into your workflow. With the addition of the Task Runner Explorer, Visual Studio will now generate these files on the fly for me as I build my application. What other tasks would you want Visual Studio to run for you in Grunt? Let me know in the comments below!


About the Author

Jeffrey T. Fritz

Jeffrey T. Fritz is a Microsoft MVP in ASP.Net and an ASPInsider with more than a decade of experience writing and delivering large scale multi-tenant web applications. After building applications with ASP, ASP.NET and now ASP.NET MVC, he is crazy about building web sites for all sizes on any device. You can read more from Jeffrey on his personal blog or on Twitter at @csharpfritz. Google Profile


Comments

Comments are disabled in preview mode.