Tuesday 21 July 2009

Script and styles optimizer for ASP.NET Part1

Abstract


Since project I'm working on became stable we've started to optimise it. Naturally one of the main aims in site optimisation is to minimize load bandwidth and minimize request count between clients and server. I started digging about zipping static content and combining it. Then I hit on two great post which inspired me to my work. You can find them here and here. Thanks a lot for those posts!

Especially the first one impressed me. In the beginning I wanted to just use it, but then I realized that I need a little more flexible and extensible solution.

My requirements were:
1. Ability to pass css and js files as a single request (per type).
2. Ability to zip content.
3. Ability to cache content.
4. Ability to configure feature from both configuration file and code.
5. Ability to dynamically adding and removing content (both declaratively and imperatively, by removing I mean that it's possible to omit some part of content on particular site.
6. Put embedded scripts (*.axd) into combined and zipped resources as well.
7. Enclose it as reusable and plug-and-play project.

I've decided to follow great idea of Moiz Dhanji (thank you again) and I extended the ScriptManager control. Later I've noticed that it's good to extend ScriptManagerProxy cotrol as well.
Based on all of those requirements and two mentioned blog posts I have created a very useful tool - OptimisedScriptManager. If you are interesting in it or you are looking for similar solution you can download and try it from here.

Using Instructions


1. Add reference to "Cognifide.EPiServerControls.OptimisedScriptManager.dll" in your web application.

2. In web.config (under the section in collection) register optimised control:

<add assembly="Cognifide.EPiServerControls.OptimisedScriptManager" namespace="Cognifide.EPiServerControls.OptimisedScriptManager.UI" tagPrefix="Optimised"/>



3. Now add two GenericHandlers, one will be responsible for css calls, the second one will be responsible for javascripts calls, lets say Css.ashx and Scripts.ashx respectively. Now:

- Please delete both .cs files related to newly added handlers.
- Open Css.ashx and change Class attribute to "Cognifide.EPiServerControls.OptimisedScriptManager.CssHandler" and delete
CodeBehind attribute.
- Open Scripts.ashx do the same with one except set Class to "Cognifide.EPiServerControls.OptimisedScriptManager.JavaScriptsHandler"
Your markups should now looks like these below:

<%@ WebHandler Language="C#" Class="Cognifide.EPiServerControls.OptimisedScriptManager.CssHandler" %>


<%@ WebHandler Language="C#" Class="Cognifide.EPiServerControls.OptimisedScriptManager.JavaScriptsHandler" %>



4. Now if you have ScriptManager somewhere in your code please replace it with

<Optimised:OptimizedScriptManager runat="server" ID="sm">


if not, please add it in the same terms as common ScriptManager control.

5. Rewrite all your scripts and styles declared in page header to the OptimisedScriptManager control, look at the sample below:

<Optimised:OptimisedScriptManager runat="server" ID="sm">
<Csses>
<Optimised:CssDocumentItem Path="~/Css/Style1.css" MediaType="screen" />
<Optimised:CssDocumentItem Path="~/Css/Style2.css" MediaType="screen" />
<Optimised:CssDocumentItem Path="~/Css/Style3.css" MediaType="print" />
</Csses>
<Scripts>
<asp:ScriptReference Path="~/Scripts/Script1.js" />
<asp:ScriptReference Path="~/Scripts/Script2.js" />
</Scripts>
</Optimised:OptimisedScriptManager>

6. Last thing to do is initialize Opimizer. You can do it both from code or from config file. Please note that you have to initialize tool before first call if you are configure it form code.

Imperative example:

OptimisedContext.Instance.ScriptsEnabled = true;

OptimisedContext.Instance.ScriptsHandlerPath = "/Scripts/Scripts.ashx";

OptimisedContext.Instance.ScriptsVersion = "1";

OptimisedContext.Instance.ScriptZipContent = true;

OptimisedContext.Instance.ScriptsClientCacheDuration = 10;

OptimisedContext.Instance.ScriptsAppCacheDuration = 1;

 

OptimisedContext.Instance.CssHandlerPath = "/Css/Css.ashx";

OptimisedContext.Instance.CssEnabled = true;

OptimisedContext.Instance.CssVersion = "1";

OptimisedContext.Instance.CssZipContent = true;

OptimisedContext.Instance.CssClientCacheDuration = 10;

OptimisedContext.Instance.CssAppCacheDuration = 1;



Declarative example:

- SectionGroup registration:

<sectionGroup name="OptimisedManager" type="Cognifide.EPiServerControls.OptimisedScriptManager.Configuration.OptimisedManagerConfigurationSectionGroup, Cognifide.EPiServerControls.OptimisedScriptManager">

    <section name="CssOptimised" type="Cognifide.EPiServerControls.OptimisedScriptManager.Configuration.OptimisedManagerConfigurationSection, Cognifide.EPiServerControls.OptimisedScriptManager" allowDefinition="Everywhere" />

    <section name="ScriptOptimised" type="Cognifide.EPiServerControls.OptimisedScriptManager.Configuration.OptimisedManagerConfigurationSection, Cognifide.EPiServerControls.OptimisedScriptManager" allowDefinition="Everywhere" />

</sectionGroup>


- Config entry:

<OptimisedManager>

    <CssOptimised Enabled="true" ZipContent="true" Version="5" HandlerPath="/Scripts/Scripts.ashx" AppCacheDuration="1" ClientCacheDuration="10" />

    <ScriptOptimised Enabled="true" ZipContent="true" Version="7" HandlerPath="/Css/Css.ashx" AppCacheDuration="1" ClientCacheDuration="10" />

</OptimisedManager>



Important notes


Optimizer:
- ignores script blocks and startup scripts.
- does not affect AJAX
- cares about MicrosoftAjax and MicrosoftAjaxToolkit scripts
- will generate separate content for each combination of scripts/styles so you don't have to worry about placing various scripts and styles on various pages
- generates separate <script> tag for each MediaType
- if you use relative paths in your styles place Css.ashx in the same folder as styles, otherwise url to images will be broken
- you can mix tool initialization between config file and code.

Future work


First of all I would like to write the same feature in ASP.NET MVC or adjust the existing one, I'm really looking forward to start doing it.
There is still a little work to do in this optimiser as well:

  • Before all I want to automate capturing calls, by registering custom UrlRewriter.

  • Fact, that css handler has to be placed in css folder isn't good, so the tool which switch the url included in css files will be huge benefit

  • I'm thinking about capturing script services proxy, startup scripts and combining them as well.

  • Caching part could be improved.

  • Logging part could be improved (added ;)).



What will be in part 2


In this part which I hope publicate in next week I explain how optimizer works internally and I'll give an intructions how to:

- use OptimisedManagerProxy control (basically if you used ScriptManagerProxy before this is pretty much the same)
- add styles and scripts from embeded resources
- make file ignored for some pages (using OptimiseManagerProxy and IsIgnored attribute)
And also how the tool works internally, which is not a big secret when you have a reflector:), and why Script.ashx is called twice - second time with empty parameters.

Summary


Optimizer is a simple and ready to use tool. It minimize both bandwidth and requests which have huge impact on loading time and user experience. This solution isn't perfect, but it works currently in really big project. I will apprietiate your feedback.