This post is the first part of a two part article in which I outline a series of possibilities to increase the performance of websites in general and in particular when it comes to websites build on Smartsite iXperion. This article is mostly technical of nature and shows several approaches in reaching the desired goal.
1 What is going on?
To perform the suggestions in this post it is necessary to have the appropriate tools. The most important tool I use is the Firebug add-on for Firefox. Using the Net tab in the add-on allows for a clear view on what is going on behind the screens of a page being loaded, in what order and how many bytes were transferred.
2 Where did those resources go?
When optimizing a webpage first verify that all the resources being used in the page can actually be found. Perhaps there are resources that were used in the early stages of the web development but became obsolete later on. If the call for this resource was never removed from the html but the resource is in fact non-existent a senseless call is made to the server. Use Firebug to check for any lines in the load sequence that are red. These resources are either faulty or non-existent.
3 Http handling a little bit too much
When using HTTP handlers to process images for a page make sure that the response header contains the correct caching directives. Before returning add the following lines to the code:
context.Response.Cache.SetCacheability(System.Web.HttpCacheability.Public);
This causes the returned image to be cached for any user on the client machine. If images are different depending on the visitor of the page use Private instead of Public.
context.Response.Cache.SetMaxAge(new
TimeSpan(999,0,0,0));
This ensures that the image is cached on the browser for an ‘indefinite’ amount of time. This is done separately for each unique url, including the querystring. Especially when the images returned are drawn textual elements they do not need to be refreshed until the text changes, which is part of the querystring.
4 If IIS can do it, let it
On the HTTP Headers tab in the properties of the website check the ‘enable content expiration’ and set the ‘Expire after’ to 9999 days. This options forces IIS to add header information to any response that does not have explicit header information yet. Any calls to image, javascript and Css resources will be given a max-age of 9999. Be aware that the standard processing of images, javascripts and CSS from within Smartsite are explicitly given ‘No-Cache’ directives and are therefore ignored by IIS Caching. See next topics on how to bypass this.
5 Smart resources in a Smart site
The major downside of IIS Caching is that when a css or js resource is updated this is not recognized by the browser because the name has remained the same. If these resources reside on disk you could solve this by adding a suffix to the filenames containing some sort of versioning information. Although this would work it is a tedious task to change the name every time you made an alteration and you would have to go through all the link tags in the HTML to make sure they are calling the new name.
By adding the CSS and javascript information to the Smartsite Cms this can be solved in a more generic way using the version nr that Smartsite updates with every change. To do so first add each CSS file to the CSS part under the Configuration branch of Smartsite and add the javascript files to the JAVASCRIPT part. Next, the HTML link tags need to be changed so the url to the resource is extended with a querystring that contains version information of the resource. As you will see we are using a scalar database call here because somehow (through experience) page errors can appear if you use the itemdata.field(‘version’) construction:
{buffer.set(
'version',
sql.scalar('SELECT
version FROM vwActive WHERE code=' + char.apostrophe() + translation.arg(1) +
char.apostrophe()))}
<link
href=”{url.setparameter(channel.link(translation.arg(1)),'v',buffer.get('version'))}”
type=”text/css” rel=”stylesheet”
/>
Now, each time the CSS is altered in Smartsite its version nr is updated and the next call for this page will append a new querystring to the url, forcing the browser to update the resource.
Although this works as a general principle we are still stuck with one problem, caused by the default header information that Smartsite adds for items rendered by the ‘Simple’ render template. This template makes sure that CSS and javascript resources are never cached. To force Smartsite to add the correct (cacheable) header information we have to change the rendertemplate to the following:
<se:xhtmlpage
xmlns:se="http://smartsite.nl/namespaces/sxml/1.0"
trim="both"
doctype="none"
omitxmldeclaration="true">
{response.setcacheable(true)}
{response.setmaxage(9999)}
{itemdata.field('logic')}
</se:xhtmlpage>Note that some of these vipers have only been introduced in version 1.01 of Smartsite iXperion.
6 CSS at the top, javascript at the bottom
Although this is argued by quite some people it is worth mentioning and in the end it depends on the site whether javascripts should be included in the header or at the bottom. This paragraph goes into putting them at the bottom since the standard implementation of Smartsite puts them in the head. One simple way to make this work would be to move the call to the javascripts translation to the bottom of the baselayout but there is one typical problem with this approach.
Because Smartsite works with placeholders for the css and javascript resources all included resources have to be added after the declaration of the placeholder itself. Since by default the placeholders are added at the top any include macros later on in the processing of the page are added without any problem. But if we move the placeholders to the bottom, any include macros that are added in the translations halfway the processing of the page cannot be resolved and the standard ‘placeholder’ solution of Smartsite for javascripts fails.
The easiest way to solve this is to create a page buffer variable that will contain a comma-delimited string with all the javascript codes for the page being rendered. During the processing the buffer variable is extended with extra item-codes where needed. A special translation ‘AddJsCode’ could facilitate in this:
{buffer.set(
'JsCodes',
Page,
buffer.get('JsCodes',
page, default='') + sys.iif(buffer.get('JsCodes', page, default='') != '', ',
') + translation.arg('script'))}
At the end of the renderprocess another the‘ProcessJsCodes’ translation is then called to process all the codes from the buffer variable and creating <link> tags for each code:
<se:if expression="buffer.get('JsCodes',page, default='') != null">
<se:format inputdata="{string.split(buffer.get('JsCodes',page),',')}">
<se:rowformat>
<se:if expression="this.parent.field(1) != ''">
{buffer.set(
'version',
sql.scalar('SELECT version FROM
vwActive WHERE code=' + char.apostrophe() + this.parent.field(1) +
char.apostrophe()))}
<link
type=”text/javascript”
href=”{url.setparameter(channel.link(this.parent.field(1)),’v’,
buffer.get(‘version’))}”
/>
</se:if>
</se:rowformat>
</se:format>
</se:if>
7 Smart images
Just like the ‘Simple’ render template does for css and javascript resources, the ‘Binary’ render template in Smartsite sets a ‘no-cache’ directive in the response header. Although this could be solved in the same manner by adding the necessary vipers this could result in images that are changed in the not being picked up by the browser. Obviously adding the version information of the item to each image url sounds like a solution but this would be unworkable for images that are being added to the body of e.g. news items. Setting cache information on the level of the render template is therefore not an option.
Based on the above it is simply not possible to create a caching mechanism for images that are added directly to the body of an item. It is possible however to set caching for all images that are added by translation in the rendering process and the easiest way to do so is by using the Smartsite Image server. By rendering all images trough the image server caching information is added to each of the response headers (Image server does so by default). As a bonus to this all images are resized to the minimum need and the amount of bytes transferred are as low as possible. A small downside to this approach is that the image server sets the max-age to only 23 hours. Not as much as hoped for but much it’s a major improvement from no caching at all.
The following translation can help facilitate this ‘imageserver’ implementation. This is just one possible way, using the image server macro is just as plausible:
<se:if expression="translation.arg(4)==true">
<se:then>
{translation.arg(1)}?hid=img;rm=17;h={translation.arg(3)};q=100;ar=1
</se:then>
<se:else>
{translation.arg(1)}?hid=img;rm=17;w={translation.arg(2)};q=100;ar=1
</se:else>
</se:if>
8 Small, smaller, smallest
IIS has something called IIS compression which compresses all data before it is transferred to the client. Although this can result in 70% data size reductions it is strangely not turned on by default and IIS versions older than 7.0 do not have a UI in their management tool to configure it. To turn on compression for a website a manual procedure is needed and are described hereunder. Be warned though that IIS will need to be stopped and restarted during the process.
1. Open IIS and open the properties for the ‘Web Sites’ folder.
2. Go to the ‘services’ tab and check both dynamic and static compression. Also set the maximum temporary directory size to a sensible value of e.g. 100 Mb
3. Stop IIS
4. Start notepad and open the file c:\windows\system32\inetsvr\metabase.xml
5. Find the IISCompressionSchemes tag and set the following attributes. This will turn of compression for all sites so sites can be turned on independendly:
a. HcDoDynamicCompression=”FALSE”
b. HcDoOnDemandCompression=”TRUE”
c. HcDoStaticCompression=”FALSE”
6. Find the 2 IISCompressionSheme tags and set the following attributes. The compressionlevel of 9 is considered the best based on extensive testing:
a. HcDoDynamicCompression=”TRUE”
b. HcDoOnDemandCompression=”TRUE”
c. HcDoStaticCompression=”TRUE”
d. HcDynamicCompressionLevel=”9”
e. HcFileExtensions=”htm html txt js xml css”
f. HcScriptFileExtensions=”asp dll exe aspx asmx css js”
7. Find the IISWebServer tag for the website and add the following attributes:
a. DoDynamicCompression=”TRUE”
b. DoStaticCompression=”TRUE”
8. Save the file
9. Start IIS
Note that the dynamic compression is used to compress data that is not directly processed from file. This also means that the compression has to be done for each response and that there is no compressed version of the data stored. The extensions processed are set by the HcScriptFileExtensions attribute. Since css and javascript resources might be processed by Smartsite (and thus virtual) these extensions are also added to the dynamic compression list.
The static compression is used to process disk resources and surely contain the css and js extensions in the HcFileExtensions attribute. If HcDoOnDemandCompression is turned on this will result in a compressed version of the resource being stored in the IIS temp folder. Other extensions are xml,html,htm. As a final warning: do not try to compress images since they are already compressed to a level that an extra compression cycle will result in more overhead then gain.
9 AJAX as it is supposed to be
Although update panels are considered a fast way to develop Ajax aware websites it is by far the thinnest implementation of Ajax technology and is even acknowledged by Microsoft to be a ‘sneaky’ Ajax solution. The reason for this lies in the amount of data that needs to be send to and from the server in order for it to work. In short: Ajax.net is based on partial rendering but to create the result HTML it needs the full viewstate to be send to the server, where it goes through the complete page lifecycle process after which it returns the full HTML with the possibly altered viewstate.
So in performance terms: wherever you can and if the architecture allows try to transfer only the data actually needed and process as much as you can on the browser side. A good approach is using Jquery and JSON to create an Ajax aware webpage. This means you use Jquery to make a call to an HTTP Handler or webservice that returns only the necessary data as JSON. This data is then processed into HTML while iterating though the data. Practice has shown that the data being send can be reduced from several KB to almost zero where as the returned data is reduced by sometimes 90%. A further insight on Jquery and JSON implementation can be found at http://docs.jquery.com/Ajax.
10 Where 2 just ain’t enough
According to the HTML1.1 standard a browser is only allowed to have 2 connections open per server at any one time. Because of this limit browsers could only retrieve 2 javascripts, images or css files at once, blocking the processing of the webpage till it’s done with all the requests. For browsers to determine if they have a connection to the same server they use the hostname in the request as referral. Although this limit has gradually been lifted by browsers like Firefox, our old friend IE6 still has it out-of-the box. Since it’s a bit cumbersome to tell all your website visitors to tweak their IE you can trick it to use more connections by differing the hostname for each of the request url’s. So calling a few on domain1.server.nl/resources and some others on domain2.server.nl/resources already doubles the amount. Actually going up to 8 different sub domains would just get it to the standard of the 16 that Firefox allows. Imagine the joy we bring to our IE6 friends.
Now, how to test this with Firebug in Firefox with its 16 connections. The only way to do this is by downgrading Firefox to a mere 2 connections. Sounds strange but it can be done. Just open “About:Config” in the Firefox address bar and you are taken to the depths of the browser. In the list of settings find “network.http.max-connections-per-server” and change the default of 16 to 2 and you are ready to see your changes have effect.
11 So far so good
This is all great but is this all really needed? No, it is not and with ever growing bandwidths and cpu power why would you care. Nevertheless, if you do implement even a view of the above measures you can definitely experience a more agile user experience and even if your webpage was already performing ok, don’t forget that whatever payload you transfer over the line it will clot the overall bandwidth and even if your server has a the best bandwidth in the world it is the bandwidth of the weakest link along the line that determines how fast the webpage is being presented.
All in all, I tested this on one of our customer websites and was surprised to see how a few efforts brought down the amount of bytes and the request by sometimes 50 to 60 percent as the following numbers show:
|
|
Kb (first time /
primed)
|
Requests (first
time/primed)
|
|
Before
|
2000/535
|
128/34
|
|
After
|
1011/265
|
103/6
|
But there is still more that can be done to make this numbers drop even further. In part 2 I will dive into viewstate tuning, asynchronous processing, script combining, minifying, data-aware caching and other tips and tricks. The trace results of Asp.Net will show to be a valuable tool for this.
SV