Many people combine all scripts (and style sheets) for the entire website into one bundle. The rationale behind this is the TCP connections limitation by browsers. Furthermore, once the bundle is downloaded, it gets cached by the browser and subsequent requests to the website are faster. This approach, however, works against the concept of modularity. Why changes in one file should invalidate the entire (potentially huge) bundle? Moreover, HTTP/2 removes the TCP connections limitation issue. Although, ASP.NET MVC provides a way to register resources separately, this build-in mechanism is not flexible enough.
Sections
ASP.NET MVC has the concept of sections, where each view using a global layout, can inject code into that layout. This enables a common template for all views, but still allowing each view to change parts of this template.
1 2 3 4 5 6 7 8 9 10 |
<!-- _Layout.cshtml --> <html> <head> <title>Hello</title> </head> <body> @RenderBody() @RenderSection("scripts", required: false) </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 |
<!-- View.cshtml --> @{ Layout = "_Layout.cshtml"; } <h1>Homepage</h1> @section scripts { <script src="common.js"></script> <script src="home.js"></script> } |
There are a couple of issues related to this concept, though:
- It cannot be applied to partial views – sections don’t work there
- You don’t have control over the cache. Once home.js gets cached, you have to manually append a version query string to the src attribute in order to invalidate the cache.
Dynamic resource registration
There are some libraries solving this issue, but in my opinion they either do too little or too much. So I decided to write a small library to help me organize my code into independent components. You can find the source code on GitHub, but in this blog post I want to focus on the use cases and the main ideas behind the implementation:
- I shall able to register individual resources, scripts and style sheets, from each view and partial view.
- I prefer the registration to happen within the view itself, rather than the controller.
- If multiple views require the same resource, it shall appear only once in the output HTML.
- I shall be able to indicate the order of inclusion of the resources.
- When a resource is changed, browsers shall get the newest version.
- AJAX requests resulting in a partial view shall determine the required resources by this view.
And here it is the actual usage.
1 2 3 4 5 6 7 8 9 10 11 |
<!-- _Layout.cshtml --> <html> <head> <title>Hello, World</title> @Html.RenderDynamicStyleSheets() </head> <body> @RenderBody() @Html.RenderDynamicScripts() </body> </html> |
1 2 3 4 5 6 7 8 9 |
<!-- View.cshtml --> @{ Layout = "_Layout.cshtml"; Html.RegisterDynamicStyleSheets("home.css"); Html.RegisterDynamicScripts("common.js", "home.js"); } <h1>Homepage</h1> |
1 2 3 4 5 6 7 |
<!-- _PartialView.cshtml --> @{ Html.RegisterDynamicStyleSheets("partialView.css"); Html.RegisterDynamicScripts("common.js", "partialView.js"); } <h1>Partial view</h1> |
Implementation
The idea is simple: I maintain a HashTable<string> containing each registered resource (one for scripts and one for style sheets) for the entire request. Hash tables keep the order of insertion and prevent duplicates. I use Request.Items to store these two hash tables for the current request. When calling the render methods, I just enumerate all registered resources and render their appropriate HTML tag (script or link).
This is indeed simple, but doesn’t help with the cache issue. A possible solution would be to append a version query string to each resource, based on a hash of its contents. It could look like this:
1 2 |
<script src="common.js?v=ewRvCezKPlxARFEFUfcAI71iX"></script> <script src="home.js?v=su6cc72VDvS0kFhDE8qqTUkA0bt"></script> |
However, I have decided to use the bundling functionality of ASP.NET MVC in System.Web.Optimization namespace. What happens then is that on the first call to a render method, I create a dynamic bundle with the registered resources. The bundle gets a unique name, based on a hash of the paths of the resources inside. This way, the bundle is registered only once. Subsequent calls to the render method will render the bundle itself using Scripts.Render or Styles.Render functionality. When optimizations are disabled (usually on your development machine), the files are rendered separately (without a version query string). When you deploy your website (and the optimizations get enabled), the result is a bundle like this:
<script src="/bundles/dynamic/rqmd9xrEY6w1?v=UkA0btXw1CC"></script>
The trickier part is when we make AJAX requests, which result in a partial view. Partial views don’t have layouts to call the render methods. In this case we can attach a global action filter that renders the resources after the partial view has been executed. This way the output HTML will contain (before or after the partial view) the required resources. However, as we have loaded the HTML via AJAX, if we just render normal script or link tags, most browsers will complain about it.
In this situation we need special rendering logic dependent resources in AJAX requests. Scripts could be rendered using jQuery’s $.getScript, while style sheets need custom piece of code to download the file asynchronously and create a new HTML link tag on the page. I have chosen a different approach here as well in order to let my client decide how to load the required resources. What I am doing is instead of rendering the resources on the resulting HTML, I am adding custom HTTP headers with the URLs of the dependent resources. The client can then load these resources and manipulate the HTML. Using the bundling mechanism in ASP.NET MVC again, this could result in multiple individual files or single bundles.
1 2 |
X-Scripts: /common.js,/home.js X-StyleSheets: /home.css |
or
1 2 |
X-Scripts: /bundles/dynamic/iO8z_SrqxrEY6w1?v=UkA0btCCXIh X-StyleSheets: /bundles/dynamic/fsd22ad6w1?v=XIh3274723ja |
Conclusion
This is a simple solution to the problem with as little code as possible and I find it useful in my daily work. Feel free you offer suggestions for improvements – I would be happy to discuss other ways to do it too. 🙂