This post was originally published on Coding Glamour.

Het samenvoegen van meerdere javascript files, en het minifyen van dezelfde javascript is een optimalisatie die flink effect kan hebben op de performance van je website; en een standaardadvies van optimalisatietools. Vandaar hier de techniek die wij gebruiken voor onze nieuwe mobiele website om javascript samen te voegen, te minifyen met YUI compressor, en automatisch te versionen.

Welke javascript is nodig?
Om op een willekeurige pagina aan te geven welke javascript benodigd is, kunnen we inhaken op het 'Page_Init()' event dat nog steeds bestaat in ASP.NET MVC. Dit draait voordat de view zal worden gerenderd. De syntax hiervoor is:

// onderaan je .aspx of .master:
<script runat="server">
    protected void Page_Init(object sender, EventArgs e)
    {
        ScriptHelper.RegisterScript("/js/jquery.js");
        ScriptHelper.RegisterScript("/js/global.js");
    }
</script>

Wanneer je bovenstaande code in je masterpage zet, kan je tevens per view een zelfde blokje toevoegen. Deze scripts worden dan ná de scripts in je masterpage toegevoegd (volgorde is nogal eens belangrijk bij javascript bestanden :-)). Renderen
Op een willekeurig punt in je pagina (of masterpage) kan je kiezen om de scripts te renderen, dankzij een toevoeging aan de HtmlHelper.

<!-- voeg dit bijvoorbeeld onderaan je pagina toe -->
<%=Html.RenderScripts() %>
<!-- hierna kan je nog losse stukken script doen doen -->

Dit print het volgende stuk code:

<script src="/resources/javascript/?keys=/js/jquery.js|/js/global.js&1234"></script>

De code '1234' achteraan is de samengestelde hash van de losse bestanden samen. Als er dus een bestand wijzigt, zal ook de hash wijzigen en zal de browser de file opnieuw binnen halen.

ScriptHelper code
Om op te slaan welke files moeten worden gerendered in de 'keys' parameter, kunnen we gebruik maken van de 'HttpContext.Current.Items', die gebonden is aan het huidige request. Hieromheen kunnen we de helper methodes schrijven die hierboven staan.

public static class ScriptHelper
{
    // hashing instance om snel de hash van een byte[] te berekenen
    private static readonly MurmurHash2UInt32Hack HashingInstance;
    static ScriptHelper()
    {
        HashingInstance = new MurmurHash2UInt32Hack();
    }
    // lijst met scripts, gebonden aan huidige request
    private static List<string> Scripts
    {
        get { return (HttpContext.Current.Items["Scripts"] ?? (HttpContext.Current.Items["Scripts"] = new List<string>())) as List<string>; }
        set { HttpContext.Current.Items["Scripts"] = value; }
    }
    
    // voeg script toe; overal beschikbaar via ScriptHelper.RegisterScript()
    public static void RegisterScript(string jsFile)
    {
        Scripts.Add(jsFile);
    }
    public static string RenderScripts(this HtmlHelper html)
    {
        // let op! zorg dat je het bepalen van de hash ergens cachet. Kost veel CPU anders.
        var scripts = Scripts.ToList();
        // dit is je cache key
        var key = string.Join("|", scripts.OrderBy(s => s).ToArray());
        StringBuilder script = GetFileContent(scripts, html.ViewContext.RequestContext.HttpContext);
        var outputFile = Encoding.UTF8.GetBytes(script.ToString());
        var hashcode = HashingInstance.Hash(outputFile);
        // renderen
        return string.Format("<script src=\"/resources/javascript/?keys={0}&{1}\"></script>", string.Join("|", scripts.ToArray()), hashcode);
    }
    public static StringBuilder GetFileContent (List<string> items, HttpContextBase httpContext)
    {
        // hier nog wat beveiliging omheen zodat ze niet al je files kunnen opvragen :-)
        StringBuilder script = new StringBuilder();
        foreach(var s in items)
        {
            string content = File.ReadAllText(httpContext.Server.MapPath("~/" + s.TrimStart('/')), Encoding.UTF8);
            script.AppendLine(content);
        }
        return script;
    }
}
/// <summary>
/// Fast hashing algorithm
/// </summary>
internal class MurmurHash2UInt32Hack
{
    public UInt32 Hash(Byte[] data)
    {
        return Hash(data, 0xc58f1a7b);
    }
    const UInt32 m = 0x5bd1e995;
    const Int32 r = 24;
    [StructLayout(LayoutKind.Explicit)]
    struct BytetoUInt32Converter
    {
        [FieldOffset(0)]
        public Byte[] Bytes;
        [FieldOffset(0)]
        public UInt32[] UInts;
    }
    public UInt32 Hash(Byte[] data, UInt32 seed)
    {
        Int32 length = data.Length;
        if (length == 0)
            return 0;
        UInt32 h = seed ^ (UInt32)length;
        Int32 currentIndex = 0;
        // array will be length of Bytes but contains Uints
        // therefore the currentIndex will jump with +1 while length will jump with +4
        UInt32[] hackArray = new BytetoUInt32Converter { Bytes = data }.UInts;
        while (length >= 4)
        {
            UInt32 k = hackArray[currentIndex++];
            k *= m;
            k ^= k >> r;
            k *= m;
            h *= m;
            h ^= k;
            length -= 4;
        }
        currentIndex *= 4; // fix the length
        switch (length)
        {
            case 3:
                h ^= (UInt16)(data[currentIndex++] | data[currentIndex++] << 8);
                h ^= (UInt32)data[currentIndex] << 16;
                h *= m;
                break;
            case 2:
                h ^= (UInt16)(data[currentIndex++] | data[currentIndex] << 8);
                h *= m;
                break;
            case 1:
                h ^= data[currentIndex];
                h *= m;
                break;
            default:
                break;
        }
        // Do a few final mixes of the hash to ensure the last few
        // bytes are well-incorporated.
        h ^= h >> 13;
        h *= m;
        h ^= h >> 15;
        return h;
    }
}


ResourcesController
In je pagina wordt nu één request gedaan, maar je krijgt nog geen antwoord van de server. Voeg eerst de YUI compressor voor .NET toe aan je project. En maak vervolgens de 'ResourcesController' aan:

public ActionResult Javascript(string keys)
{
    // ook hier, zelf cachen gaarne
    var scriptFiles = keys.Split('|').ToList();
    var scripts = ScriptHelper.GetFileContent(scriptFiles, HttpContext).ToString();
    // minification, je kan in een setting zetten of je dit wel / niet wil doen; handig voor debuggen
    scripts = JavaScriptCompressor.Compress(scripts);
    // retouneer met het juiste content type
    return new ContentResult()
        {
            Content = scripts,
            ContentType = "text/javascript"
        };
}


Et voila
De javascript wordt voortaan in 1 request opgehaald, en geminified naar de client gestuurd. Scheelt weer wat onnodig snelheidsverlies zonder verlies in functionaliteit.