Concatenate and minify JavaScript on the fly OR at build time - ASP.NET MVC

24,398

Solution 1

In the appendix of Professional ASP.NET 3.5 Scott Hanselman talks about Packer for .NET. This will integrate with MSBuild and pack javascript files for production deployments etc.

Solution 2

Try this:

I’ve recently completed a fair bit of research and consequent development at work that goes quite far to improve the performance of our web application’s front-end. I thought I’d share the basic solution here.

The first obvious thing to do is benchmark your site using Yahoo’s YSlow and Google’s PageSpeed. These will highlight the "low-hanging fruit" performance improvements to make. Unless you’ve already done so, the resulting suggestions will almost certainly include combining, minifying and gzipping your static content.

The steps we’re going to perform are:

Write a custom HTTPHandler to combine and minify CSS. Write a custom HTTPHandler to combine and minify JS. Include a mechanism to ensure that the above only do their magic when the application is not in debug mode. Write a custom server-side web control to easily maintain css/js file inclusion. Enable GZIP of certain content types on IIS 6. Right, let’s start with CSSHandler.asax that implements the .NET IHttpHandler interface:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;

namespace WebApplication1
{
    public class CssHandler : IHttpHandler
    {
        public bool IsReusable { get { return true; } }

        public void ProcessRequest(HttpContext context)
        {
            string[] cssFiles = context.Request.QueryString["cssfiles"].Split(',');

            List<string> files = new List<string>();
            StringBuilder response = new StringBuilder();
            foreach (string cssFile in cssFiles)
            {
                if (!cssFile.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
                {
                    //log custom exception
                    context.Response.StatusCode = 403;
                    return;
                }

                try
                {
                    string filePath = context.Server.MapPath(cssFile);
                    string css = File.ReadAllText(filePath);
                    string compressedCss = Yahoo.Yui.Compressor.CssCompressor.Compress(css);
                    response.Append(compressedCss);
                }
                catch (Exception ex)
                {
                    //log exception
                    context.Response.StatusCode = 500;
                    return;
                }
            }

            context.Response.Write(response.ToString());

            string version = "1.0"; //your dynamic version number 

            context.Response.ContentType = "text/css";
            context.Response.AddFileDependencies(files.ToArray());
            HttpCachePolicy cache = context.Response.Cache;
            cache.SetCacheability(HttpCacheability.Public);
            cache.VaryByParams["cssfiles"] = true;
            cache.SetETag(version);
            cache.SetLastModifiedFromFileDependencies();
            cache.SetMaxAge(TimeSpan.FromDays(14));
            cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
        }
    }
}

Ok, now some explanation:

IsReUsable property:

We aren’t dealing with anything instance-specific, which means we can safely reuse the same instance of the handler to deal with multiple requests, because our ProcessRequest is threadsafe. More info.

ProcessRequest method:

Nothing too hectic going on here. We’re looping through the CSS files given to us (see the CSSControl below for how they’re coming in) and compressing each one, using a .NET port of Yahoo’s YUICompressor, before adding the contents to the outgoing response stream.

The remainder of the method deals with setting up some HTTP caching properties to further optimise the way the browser client downloads (or not, as the case may be) content.

We set Etags in code so that they may be the same across all machines in our server farm. We set Response and Cache dependencies on our actual files so, should they be replaced, cache will be invalidated. We set Cacheability such that proxies can cache. We VaryByParams using our cssfiles attribute, so that we can cache per CSS file group submitted through the handler. And here is the CSSControl, a custom server-side control inheriting the .NET LiteralControl.

Front:

<customcontrols:csscontrol id="cssControl" runat="server">
  <CustomControls:Stylesheet File="main.css" />
  <CustomControls:Stylesheet File="layout.css" />
  <CustomControls:Stylesheet File="formatting.css" />
</customcontrols:csscontrol>

Back:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Linq;
using TTC.iTropics.Utilities;

namespace WebApplication1
{
    [DefaultProperty("Stylesheets")]
    [ParseChildren(true, "Stylesheets")]
    public class CssControl : LiteralControl
    {
        [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
        public List<Stylesheet> Stylesheets { get; set; }

        public CssControl()
        {
            Stylesheets = new List<Stylesheet>();
        }

        protected override void Render(HtmlTextWriter output)
        {
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                const string format = "<link rel=\"Stylesheet\" href=\"stylesheets/{0}\"></link>";

                foreach (Stylesheet sheet in Stylesheets)
                    output.Write(format, sheet.File);
            }
            else
            {
                const string format = "<link type=\"text/css\" rel=\"Stylesheet\" href=\"stylesheets/CssHandler.ashx?cssfiles={0}&version={1}\"/>";
                IEnumerable<string> stylesheetsArray = Stylesheets.Select(s => s.File);
                string stylesheets = String.Join(",", stylesheetsArray.ToArray());
                string version = "1.00" //your version number

                output.Write(format, stylesheets, version);
            }

        }
    }

    public class Stylesheet
    {
        public string File { get; set; }
    }
}

HttpContext.Current.IsDebuggingEnabled is hooked up to the following setting in your web.config:

<system.web>
  <compilation debug="false">
</system.web>

So, basically, if your site is in debug mode you get HTML markup like this:

<link rel="Stylesheet" href="stylesheets/formatting.css"></link>
<link rel="Stylesheet" href="stylesheets/layout.css"></link
<link rel="Stylesheet" href="stylesheets/main.css"></link>

But if you’re in production mode (debug=false), you’ll get markup like this:

<link type="text/css" rel="Stylesheet" href="CssHandler.ashx?cssfiles=main.css,layout.css,formatting.css&version=1.0"/>

The latter will then obviously invoke the CSSHandler, which will take care of combining, minifying and cache-readying your static CSS content.

All of the above can then also be duplicated for your static JavaScript content:

`JSHandler.ashx:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;

namespace WebApplication1
{
    public class JSHandler : IHttpHandler
    {
        public bool IsReusable { get { return true; } }

        public void ProcessRequest(HttpContext context)
        {
            string[] jsFiles = context.Request.QueryString["jsfiles"].Split(',');

            List<string> files = new List<string>();
            StringBuilder response = new StringBuilder();

            foreach (string jsFile in jsFiles)
            {
                if (!jsFile.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
                {
                    //log custom exception
                    context.Response.StatusCode = 403;
                    return;
                }

                try
                {
                    string filePath = context.Server.MapPath(jsFile);
                    files.Add(filePath);
                    string js = File.ReadAllText(filePath);
                    string compressedJS = Yahoo.Yui.Compressor.JavaScriptCompressor.Compress(js);
                    response.Append(compressedJS);
                }
                catch (Exception ex)
                {
                    //log exception
                    context.Response.StatusCode = 500;
                    return;
                }
            }

            context.Response.Write(response.ToString());

            string version = "1.0"; //your dynamic version number here

            context.Response.ContentType = "application/javascript";
            context.Response.AddFileDependencies(files.ToArray());
            HttpCachePolicy cache = context.Response.Cache;
            cache.SetCacheability(HttpCacheability.Public);
            cache.VaryByParams["jsfiles"] = true;
            cache.VaryByParams["version"] = true;
            cache.SetETag(version);
            cache.SetLastModifiedFromFileDependencies();
            cache.SetMaxAge(TimeSpan.FromDays(14));
            cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
        }
    }
}

And its accompanying JSControl:

Front:

<customcontrols:JSControl ID="jsControl" runat="server">
  <customcontrols:Script File="jquery/jquery-1.3.2.js" />
  <customcontrols:Script File="main.js" />
  <customcontrols:Script File="creditcardpayments.js" />
</customcontrols:JSControl>

Back:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Linq;

namespace WebApplication1
{
    [DefaultProperty("Scripts")]
    [ParseChildren(true, "Scripts")]
    public class JSControl : LiteralControl
    {
        [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
        public List<Script> Scripts { get; set; }

        public JSControl()
        {
            Scripts = new List<Script>();
        }

        protected override void Render(HtmlTextWriter writer)
        {
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                const string format = "<script src=\"scripts\\{0}\"></script>";

                foreach (Script script in Scripts)
                    writer.Write(format, script.File);
            }
            else
            {
                IEnumerable<string> scriptsArray = Scripts.Select(s => s.File);
                string scripts = String.Join(",", scriptsArray.ToArray());
                string version = "1.0" //your dynamic version number
                const string format = "<script src=\"scripts/JsHandler.ashx?jsfiles={0}&version={1}\"></script>";

                writer.Write(format, scripts, version);
            }
        }
    }

    public class Script
    {
        public string File { get; set; }
    }
}

Enabling GZIP:

As Jeff Atwood says, enabling Gzip on your web site server is a no-brainer. After some tracing, I decided to enable Gzip on the following file types:

.css .js .axd (Microsoft Javascript files) .aspx (Usual ASP.NET Web Forms content) .ashx (Our handlers) To enable HTTP Compression on your IIS 6.0 web server:

Open IIS, Right click Web Sites, Services tab, enable Compress Application Files and Compress Static Files Stop IIS Open up IIS Metabase in Notepad (C:\WINDOWS\system32\inetsrv\MetaBase.xml) – and make a back up if you’re nervous about these things Locate and overwrite the two IIsCompressionScheme and one IIsCompressionSchemes elements with the following:

And that’s it! This saved us heaps of bandwidth and resulted in a more responsive web application throughout.

Enjoy!

Solution 3

Why not use the ScriptManager? Here's an MVCScriptManager that will combine AND squish.

Solution 4

Use either YUI Compressor or Dojo compressor. They both use the Rhino JS parsing engine which tokenizes your code, and will therefore only work if the code is valid code. If there is an error, they'll let you know (which is a nice bonus IMO!) Packer on the other hand, will pack your code even if it contains errors.

I use YUI in all my projects via build scripts. Never do it on the fly, it takes too long to do the compression. Both YUI and Dojo are Java based (ala Rhino) and if you do it on the fly, you'll be spawning background processes to generate the output - not good for performance. Always do it at build time.

Solution 5

Rejuicer is a great new minifier for ASP.NET that's getting a lot of buzz: http://rejuice.me

It is configured as a HTTP module & performs minification at run-time (once) and caches the output.

It:

  • Has a fluent interface for configuration
  • Allows you to specify files to minify with wildcard rules
  • Runs on Windows Azure
  • Somewhat magically turns itself off in development environments, so you can debug your original javascript code (not minified).

The configuration (done on ApplicationStart in global.asax.cs) is as simple as:

OnRequest.ForJs("~/Combined.js")
            .Compact
            .FilesIn("~/Scripts/")
              .Matching("*.js")
            .Cache
            .Configure();
Share:
24,398

Related videos on Youtube

Charlino
Author by

Charlino

Updated on October 17, 2020

Comments

  • Charlino
    Charlino over 3 years

    As an extension to this question here Linking JavaScript Libraries in User Controls I was after some examples of how people are concatenating and minifying JavaScript on the fly OR at build time. I would also like to see how it then works into your master pages.

    I don't mind page specific files being minified and linked inidividually as they currently are (see below) but all the JavaScript files on the main master page (I have about 5 or 6) I would like concatenated and minified.

    Bonus points for anyone who also incorporates CSS concatenation and minification! :-)

    Current master page with the common JavaScript files that I would like concatenated and minified:

    <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
    <head runat="server">
        ... BLAH ...
        <asp:ContentPlaceHolder ID="AdditionalHead" runat="server" />
        ... BLAH ...
        <%= Html.CSSBlock("/styles/site.css") %>
        <%= Html.CSSBlock("/styles/jquery-ui-1.7.1.css") %>
        <%= Html.CSSBlock("/styles/jquery.lightbox-0.5.css") %>
        <%= Html.CSSBlock("/styles/ie6.css", 6) %>
        <%= Html.CSSBlock("/styles/ie7.css", 7) %>
        <asp:ContentPlaceHolder ID="AdditionalCSS" runat="server" />
    </head>
    <body>
        ... BLAH ...
        <%= Html.JSBlock("/scripts/jquery-1.3.2.js", "/scripts/jquery-1.3.2.min.js") %>
        <%= Html.JSBlock("/scripts/jquery-ui-1.7.1.js", "/scripts/jquery-ui-1.7.1.min.js") %>
        <%= Html.JSBlock("/scripts/jquery.validate.js", "/scripts/jquery.validate.min.js") %>
        <%= Html.JSBlock("/scripts/jquery.lightbox-0.5.js", "/scripts/jquery.lightbox-0.5.min.js") %>
        <%= Html.JSBlock("/scripts/global.js", "/scripts/global.min.js") %>
        <asp:ContentPlaceHolder ID="AdditionalJS" runat="server" />
    </body>
    

    Used in a page like this (which I'm happy with):

    <asp:Content ID="signUpContent" ContentPlaceHolderID="AdditionalJS" runat="server">
        <%= Html.JSBlock("/scripts/pages/account.signup.js", "/scripts/pages/account.signup.min.js") %>
    </asp:Content>
    


    UPDATE: Recommendations for now (late 2013):

    I would look at Microsoft ASP.NET's built in Bundling and Minification.

    • Bayard Randel
      Bayard Randel almost 15 years
      Very interested to see what people are doing here. The port of YUI compress looks like the best place to start.
    • Charlino
      Charlino almost 15 years
      Anyone have any solutions using YUI?
    • Mark
      Mark about 11 years
      Does something being the duck's nuts mean it's good or bad?
    • Charlino
      Charlino about 11 years
      It's good :-) Although this answer is probably outdated now... there are better buck nuts out there.
    • Jonesome Reinstate Monica
      Jonesome Reinstate Monica over 10 years
      I have to ask if "duck's nuts" is acceptable... <g>
    • Jonesome Reinstate Monica
      Jonesome Reinstate Monica over 10 years
      @Charlino What is the better way now? I dug around, and all the SO threads and blog posts I could find are based on old 3.5 approach.
    • Charlino
      Charlino over 10 years
      @Jonesome - I would say the best approach now would be to use the ASP.NET Bundling and Minification. See here: asp.net/mvc/tutorials/mvc-4/bundling-and-minification
  • Charlino
    Charlino almost 15 years
    That looks great, I'll have to give it a whirl. I've heard bad things about 'Packer' but I see it supports 'JSMin' too.
  • Bayard Randel
    Bayard Randel almost 15 years
    While that looks nice, one advantage that YUI Compress seems to have is that it does CSS compression and concatenation as well.
  • Charlino
    Charlino almost 15 years
    Packer for .NET does do CSS concatenation and minifying as well - check out the link :-) But yeah, I do hear that YUI Compress does a better job than anything else at minifying JS & CSS.
  • Charlino
    Charlino almost 15 years
    That looks like a great option for on the fly concatenation and minification. But I'm definitely swaying towards a build time solution. Much cleaner without the overhead, plus I can do CSS there while I'm at it :-)
  • Charlino
    Charlino about 14 years
    Wow - that's an incredibly detailed response, definately worthy of a blog post somewhere! Definitely a good solution if it fits your website. With my website all the js and css that needs to be combined, is combined anyway so I don't really need such a complex solution. And yes, I've enabled gzip. Plus I've put far future expire headers and automatic versioning of my js & css files on a cookie free domain - but that's another question all together!
  • Mark Gibaud
    Mark Gibaud over 10 years
    A few years later and the world has moved on, although I needed to re-solve this problem at my new employer. Hands down I would now advise using Cassette: getcassette.net