Click here to Skip to main content
15,896,111 members
Articles / Web Development / HTML
Tip/Trick

ASP.NET MVC RequireJS Module Optimization

Rate me:
Please Sign up or sign in to vote.
4.25/5 (3 votes)
6 Jan 2015CPOL6 min read 32.5K   509   12   6
Improve ASP.NET MVC based SPA application performance by using script Bundles with requireJS

Introduction

As many of you know, RequireJS is a library for implementing the module pattern in JavaScript; it is an essential component when working with large client side apps, like Single Page Applications. RequireJS facilitates the job of managing script dependencies, but when used in an ASP.NET based app, it introduces a new set of challenges. This tip will demonstrate how to integrate RequireJS effectively and improve performance at the same time.

Background

I have a SPA template based on ASP.NET MVC and KnockoutJS (github) project, which is nothing more than a modified version of the original ASP.NET Web Application template, but converted into a single page app, it uses KnockoutJS to manage the client side Views/Models and routing; this is a simple project demonstrating how to replicate the Angular approach with the performance power of Knockout.

We'll focus on how I integrated RequireJS in this project.

Using RequireJS

The first step is simple, remove all script tags from the page and place just two:

/Views/Shared/_Layout.cshtml :

HTML
<script type="text/javascript" src="/Scripts/app/require.config.js" </script>
<script type="text/javascript" src="/Scripts/lib/require.js" </script>

The order here is important, we load up the configuration settings first then the actual require.js library, which will automatically detect those settings and apply them; what's in the configuration file? The mapping of our modules; all the libraries that were being requested manually, will now be managed by RequireJS as modules.

/Scripts/app/require.config.js :

JavaScript
var require = {
    paths: {
        "bootstrap": "/Scripts/lib/bootstrap.min",
        "crossroads": "/Scripts/lib/crossroads.min",
        "hasher": "/Scripts/lib/hasher.min",
        "jquery": "/Scripts/lib/jquery-1.10.2",
        "knockout": "/Scripts/lib/knockout-3.2.0",
        "knockout-projections": "/Scripts/lib/knockout-projections",
        "signals": "/Scripts/lib/signals.min",
        "text": "/Scripts/lib/text",
        "router": "/Scripts/app/router"
    },
    shim: {
        "bootstrap": { deps: ["jquery"] }
    }
};

The 'paths' setting defines all the required modules, each loaded from an individual source. We provide names (IDs) for each one which is how they are referenced and requested by our app main scripts.

The 'shim' option allows to configure dependencies manually in special cases like bootstrap, in this example we are specifying that it depends on jquery and RequireJS needs to provide it before loading bootstrap (more on this later).

Now we define the 'entry point' to our application, the main script for starting up the app:

/Scripts/app/home/startup.js

JavaScript
define('startup', ['jquery', 'knockout', 'router', 'bootstrap'], function ($, ko, router) {

    // component configuration & init
    .....

    console.info("app started");
});

I omitted the 'component configuration & init' code for clarity here, the main purpose of this block is to show how to create a module; the magic happens in the first line, we are using the define() method which RequireJS interprets to establish a module for us; we give it name, then an array of strings with the IDs of any dependencies that this new module needs to work with, as you can see here we are asking for jquery, knockout, router and bootstrap; these modules were mapped in the configuration block, so RequireJS knows how to locate them. The last parameter is the function with our code to be invoked, and as you see we are accepting three parameters ($, ko, router), RequireJS will inject into these the dependencies we requested so we can use them in our code.

Bootstrap is requested but not injected, and that's because we won't be interacting with the bootstrap object directly in this code. (it just needs to be made available)

So now the last step is to initialize our app by calling our statup.js:

/Views/Home/Index.cshtml :

HTML
<script type="text/javascript">  
   // Load the main app module to start the app
   requirejs(["startup"]);
</script>

If everything works as expected, the site will fire up and the landing page displays. RequireJS runs our entry point script (startup.js) and begins loading dependent modules Asynchronously which is what we want, but in this case it presents an issue since our startup module uses most of those dependencies from the beginning, the net requests window shows the following when first loading:

NET Requests

After the initial load, most of those will be cached in the browser for subsequent requests, so the performance hit is found at the start; this might not be such a big problem here since we are only using a few modules, but modern applications may have dozens of dependencies so the logical next step is to try to optimize and minimize those requests. How can we do this in ASP.NET MVC?

Using the Code

If you are in the Mean stack, a solution is provided by using RequireJS Optimization framework, and there are ways to use this approach with ASP.NET, but you'll have to use Node.js and custom build processes.

So the obvious answer that most of you already thought of is, let's use ASP.NET Bundling and Minification! We can put all those scripts into a bundle and allow ASP.NET to serve us a single optimize file, like so:

/App_Start/BundleConfig.cs:

C#
bundles.Add(new ScriptBundle("~/bundles/libs").Include(        
        "~/Scripts/lib/crossroads.js",
        "~/Scripts/lib/hasher.js",
        "~/Scripts/lib/jquery.js",
        "~/Scripts/lib/knockout.js",
        "~/Scripts/lib/knockout-projections.js",
        "~/Scripts/lib/signals.js",
        "~/Scripts/lib/text.js"
 ));

BundleTable.EnableOptimizations = true;

There is a problem though, we cannot just put a Scripts.Render("~/bundles/libs") into our page, because that would create a separate <Script> tag to load the bundle, RequireJS will show an error because it insists in being sole manager of script dependencies (which makes sense since they are loaded asynchronously).

So the solution is to change RequireJS's configuration to ask for these using the 'bundles' option:

/Scripts/app/require.config.js:

JavaScript
var require = {
    bundles: {
        '/bundles/libs?v=2015': [
            "signals",
            "crossroads",
            "hasher",
            "jquery",
            "knockout",
            "knockout-projections",            
            "text"
        ]
    },
    paths: {
        "bootstrap": "/Scripts/lib/bootstrap.min",
    },
    shim: {
        bootstrap: { deps: ["jquery"] }
    }
};

In this new configuration, the important change is we are telling RequireJS to look for all those modules in a single location: '/bundles/libs?v=2015', which is the hard-coded path pointing to our ASP.NET Bundle ('?v=2015' optional, force refreshing) it will serve the minified bundle as defined with all the module scripts combined. After compiling and running again, the results are substantial:

optimized requests

* There are a few other script bundles defined not shown in the configuration scripts. The same principle applies, the main objective is to minimize round trips to the server for individual resources.

* Notice also that bootstrap.min.js dependency still is being retrieved individually, and that is because bootstrap does not implement the module pattern (as of this writing), it is only available globally, so we need to let RequireJS decide when to load it.

That worked great, except...

Points of Interest

...There is a catch, there is always a catch; in this case you need to make sure your source libraries and scripts are 'named modules', that is they are 'defined' with the name of the module to be requested later on.

I found this the hard way after configuring as shown above I had all sorts of errors, after some debugging I noticed some of the dependencies were not being loaded (Knockout for instance) even though the code was in the bundle, what happened?

This is how some of the libraries were defining their modules (from their original sources):

JavaScript
// knockout.js
if (typeof define === 'function' && define['amd']) {
     // [2] AMD anonymous module                
     define(['exports', 'require'], factory);
}

// hasher.js
if (typeof define === 'function' && define.amd) {
    define(['signals'], factory);
}

This was the problem, those modules were unnamed, which when bundled together prevented RequireJS from finding them among the rest. This is not an issue when the modules are loaded individually, since you usually 'map' the module to the file... (as shown in the initial configuration block) but if we put all those scripts into a single bundled file, then RequireJS will not be able to find any anonymous (unnamed) modules.

The quick solution, go into each source library and verify and fix by providing a default name like this:

JavaScript
// knockout.js
if (typeof define === 'function' && define['amd']) {             
     define("knockout", ['exports', 'require'], factory);
}

// hasher.js
if (typeof define === 'function' && define.amd) {
    define("hasher", ['signals'], factory);
}

The only other drawback to this method is the fact that you'll have to define your scripts in two places, your ASP.NET bundle and the RequireJS configuration; other than that, this solution is simple enough to implement without any external requirements.

History

  • Revision 1: Created 1/5/2015
  • Updated 1/6/2015
  • Revision 2: Created 1/7/2015
  • Updated 1/9/2015

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer Sifabs Systems Inc.
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHard coded bundle path Pin
Member 1087509229-Apr-15 2:15
Member 1087509229-Apr-15 2:15 
AnswerRe: Hard coded bundle path Pin
Riverama29-Apr-15 2:50
professionalRiverama29-Apr-15 2:50 
I think you could, a simple solution would be to place a script tag before the require.config one, in there load up a global variable with the dynamically generated bundle path.
Then in require.config use that variable to read the path and replace the hard-coded one.

haven't tested that.. let us know if that works.
GeneralRe: Hard coded bundle path Pin
Member 1087509229-Apr-15 3:10
Member 1087509229-Apr-15 3:10 
QuestionHow to naming for un-amd module Pin
duahaudo10-Apr-15 16:12
duahaudo10-Apr-15 16:12 
AnswerRe: How to naming for un-amd module Pin
Riverama21-Apr-15 8:40
professionalRiverama21-Apr-15 8:40 
GeneralRe: How to naming for un-amd module Pin
Member 1273113828-Aug-17 4:51
Member 1273113828-Aug-17 4:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.