Configuration‎ > ‎

Skins and Caching

posted Mar 23, 2011, 6:07 AM by Eric Patrick   [ updated Oct 4, 2011, 12:33 PM ]
Overview

The qbo.ApplicationWeb was extended in late 2010 to optimize page load times.  Current skins (including the standard skin) leverage these techniques (described below), but have a few gotchas associated with them.  The purpose of this post is to help troubleshoot and avoid these gotchas.  The gotchas are at the bottom of the post, but I strongly recommend developers read the entire post to understand what's going on.

JavaScript and CSS Caching

QBO sites include a LOT of javascript and css. If you use Fiddler to analyze the traffic between a browser and our servers on any given page draw, a single page will often result in 10 or more hits to the server.  Most of these hits are for static files, most notably javascript, css files, and images.  Assuming a server is properly configured, the server will tell the browser that the file has not changed since the last hit (a '304' response), so the browser is not re-downloading these resources on every page.  However, each 304 hit does consume bandwidth, and prevents the browser from considering the page loading complete prior to receiving responses.

To minimize the number of 304 calls to the server, qbo.ApplicationWeb offers the option of serving up all javascript or css in a single hit:
  • /Application/SkinService.asmx/Javascript: this combines all the javascript files in a single call
  • /Application/SkinService.asmx/SiteStyle: this combines all the css files in a single call
More importantly, this web service tells the browser to cache these files for 24 hours. After receiving the results of these web service calls, the browser will stop making these call (no more 304s) for 24 hours.  This noticeably speeds page load time.

Main Menu

Each page draw that includes a site's main menu includes a lot of HTML to handle the drop-down menu under each main menu item. In fact, on the average page, the majority of HTML streamed to the browser is just main menu related content.  Rather than streaming this content to the browser on every page draw, we can now load this content via JSON, and more importantly, cache that JSON on the browser so this content is loaded once per day.  This decreases our average page size by about 50%.

To execute this technique, the are a few components that come into play:
  • {skin}.SiteStructure.xslt: 
    • the 'MenuBar' template should emit javascript to fetch the main menu drop-down menu data once a page has been loaded
    • a template named 'MainMenu' delivers an XML node that represents each of the drop-down menu options
  • /Application/SkinService.asmx/MainMenu: this calls the SiteStructure.xslt's 'MainMenu' template, and transforms the resulting XML into JSON
  • {skin}.SiteStructure.xml: defines the LoanMainMenu and RenderMainMenu functions (see below)
When the page is rendered on the browser, each page that contains a main menu (e.g. has the skin rendered) contains a lines like these, from {skin}.SiteStructure.xslt:

<script type="text/javascript" src="/Application/SkinService.asmx/Javascript"></script>
...
<script type="text/javascript">qbo.UserID = '056';</script> ...
<script type="text/javascript">window.addEvent('domready', LoadMainMenu);</script>

Within the JavaScript cached by the page will be these functions:

// This will tell the browser to get the main menu JSON; if it's cached, the browser will just use it from cache
function LoadMainMenu()
{
    var id = (qbo && qbo.UserID) ? qbo.UserID : 0;
    new Request.JSON({
    url: '/Application/SkinService.asmx/MainMenu?' + id,
    method: 'get',
    secure: false,
    data: {},
    onSuccess: RenderMainMenu
    }).send();
}

// This will take the JSON returned, and render drop-down menus for each menu item
function RenderMainMenu(json, text)
{
    if ((json.MenuCollection == null) || (json.MenuCollection.MenuItem == null)) return;
    var mi = $splat(json.MenuCollection.MenuItem);
    mi.each(function(m){
        var c = new qbo.ContextMenu(m.Target, {event: 'leftClick', position: 'left bottom'});
        m.MenuOption.each(function(i){
            c.addItem({label: i.Label, href: i.Url});
        });
    });
}

The JSON served up by /Application/SkinService.asmx/MainMenu is something like this:

{
"MenuCollection": 
{
"modules": 12,
"qboWeb": "http://qbo.web.quandis.com/",
"MenuItem": 
[
{
"Target": "MenuItem_ContactModule",
"MenuOption": 
[
{
"Url": "/Contact/OrganizationSearch.aspx",
"Title": "",
"Label": "Organizations"
},

{
"Url": "/Contact/Broker/BrokerSearch.aspx",
"Title": "",
"Label": "Brokers"
},
...
]
},
{
"Target": "MenuItem_WorklistModule",
"MenuOption": 
[
{
"Url": "/Decision/SmartWorklistAssignByPerson.aspx?Action=Assign",
"Title": "Click here to navigate to your next Smart Worklist item.",
"Label": "Next Work Item"
},
...
]
}
]
}
}

Gotcha #1: 'this.trigger' is undefined JavaScript error

The drop-down (or 'context') menus under each Main Menu item is drawn by JavaScript, specifically the qbo.ContextMenu class.  The menu should show up when you click on a Main Menu item, or 'trigger'.  Note in the JSON above that each object in the MenuItem array contains a 'Target' property.  This should be a unique id of the HTML element that should be the trigger for the menu.  If the JSON contains triggers that don't actually exist, the context menu will attempt to bind to an undefined element, and you have your error.

To resolve this problem, press Ctrl-F5 in your browser.  If this does not work, shut your browser down and restart it.

The fundamental cause for this is the browser cache of the main menu JSON is different than the main menu being rendered in the skin.  

This typically happens during development, when a developer modified the {skin}.SiteStructure.xml.  Keep in mind that the main menu HTML is produced on every page draw by the server, and "popping web.config" will clear the server-side cache, but does nothing to the browser-side cache of the main menu JSON.

Pressing F5 in the browser (or clicking the reload button) tells your browser to ignore items in the cache, and fetch them again from the server.  This works like a charm -- expect in IE.  We love IE.  IE developers, in their infinite wisdom, treat the browser cache and the XMLHTTP cache differently.  Note that we fetch the JSON via a call to the Request.JSON class.  This class in turn uses an XMLHTTP object to fetch data (this is 'AJAX').  So pressing F5 in IE refreshes your HTML, Javascript, and CSS, but does not refresh anything fetched via XMLHTTP.   

Gotcha #2: 'qbo is undefined' JavaScript error (in development)

This happens if the {skin}.SiteStructure.xml contains references to javascript files that don't actually exist.  For example, when creating a skin by copying and pasting from other skins, you may inadvertently reference a JavaScript file that is not on your installation, such as qbo.Service.js or qbo.SkipTrace.js.  

You can quickly see this issue by navigating to /Application/SkinService.asmx/Javascript: you will see an error returned.

To resolve this problem, either install the referenced JavaScript file, or remove the reference from {skin}.SiteStructure.xml.

Gotcha #3: 'qbo is undefined' JavaScript error (in production)

On rare occasion, /Application/SkinService.asmx/Javascript will ignore all extra JavaScript files referenced in {skin}.SiteStructure.xml.  We do not yet know why this happens, but are researching is.  Fortunately, the "fix" is easy.

To resolve this problem, navigate to /Application/Recycle.aspx


Comments