Sandor Voordes
 

Smartsite and Image Server even smarter

15

Apr

Recognize this: you ‘re working on a new website in Smartsite and after weeks of hard work you proudly  present it to the customer so he or she can add the needed content. Of course you  build the cms as robust as possible and to keep the site as pristine as possible you  gave clear instructions on the image dimensions that they should use for each content type. But then you find out that providing them with clear instructions doesn’t mean the customer will really listen to them all the time. Soon they find that time is lacking and they start to upload and use images that are nog exactly right and within no time pages start to look skewish and gone is your proud work. Sometimes tempting you to crop and size them for the customer just to keep the site looking as you once intended.


The Image Translation

For me this was enough reason to see if I could overcome this recurring problem. Knowing about the Smartsite Image server I knew that the answer was probably to be found in that direction. And yes with its power to crop and size I figured that rendering all manageable images through the image server would allow me to transform each of them to the exact needed size. That and the existence of the image macro actually this a 5 minute job. Hence, my own image translation was born.
However,  My first image translation was still quite unsmart and was basically supplied with a width and height parameter and one though which you could determine which of the 2 dimensions would overrule the other. This was based on the inability of the Image Server to crop after scaling. As you know the Image Server allows for several orders of processing the parameters but the Smartsite team apparently decided that cropping after scaling is a ignorable situation. Anyway, having the 3 parameters I could set what dimension was the most important for each image. If it had room downwards then the width was predominant and vice versa. Not perfect but at least web pages didn’t fall apart so easily anymore.

Smarting it up

But ideally I still felt that cropping the image after sizing it would be the best option. That way I could still decide on the most dominant dimension but any ‘extra image’ in the other dimension could then be cut off. Maybe it would result in some unwanted cropping but at least the page would not break. But, as mentioned, the Image Server does not provide this and therefore I went searching for a way to crop the image before sizing. In order to do this I figured I needed to retrieve the dimensions of original image. I could then calculate the crop parameters from the needed dimensions and create an image that would be perfectly sizable. You can imagine my joy when I found that the ImageProperties macro could provided me with just that information.
Using the ImagePropties macro I knew I now had the final ingredients to create a new powerful image translation. One that would create the perfect image url every time again. Customers would be able to upload whatever image they wanted without having to worry about their website and better even, without me worrying about their website.

Getting mature

However, taking away the customer responsibility to provide the correct images also meant that just any image was uploaded, images that were far from the resulting dimensions. Although they were cropped and sized perfectly it was inevitable that soon the first images appeared that were cropped on the wrong side. A lot of times the right or bottom side of the image is a whole lot more interesting. Unfortunately my image translation rigorously cut it all off.
In came the final addition to my image macro: the croptype parameter. This parameter allows the user to set a value of 1,2 or 3 for the croptype, where 1 is for top or left, 2 is for center or middle and 3 is for bottom or right. I know that maybe 9 options would even be better (allowing the 9 parts of a 3 by 3 quadrant but I’ll explain how in most cases 3 works just fine. This is because I think that any cropping should be left to a minimum and that as much of the original image should stay intact. Taking this in mind it is clear that in every case only horizontal or vertical cropping is done and for each of these directions I think that cropping either of the 2 sides or an equal part on both is suffice in most situations. However, feel free to take my sample code and provide more options.

Conclusion

Using my image translation has really taken away my burden of having to think about image dimensions and whether they fit nicely in the html of the page. Before I sometimes used ugly solutions like ‘overflow:none’ to provide something similar but there were many occasions in which it was not usable. Besides it making my job easier I have found that the web editors love it. Through simple settings on the image content type or on the page content type allows the editor to choose which part of the image they think should be kept. By not having to crop and size the image with external tools or by filling in all the Image Server parameters themselves allow them more time to focus on their real responsibility: the text.

Image (imageLocator, width, height, cropType)

<!--// Retrieve the dimensions of the original image -->

<se:imageproperties

  location="{translation.arg(1)}"

  save="props"

  aim="off"

  resulttype="datatable" />


<!--// Put the main data into variables and convert them to floats to avoid unwanted rounding -->

{buffer.set('orgx',convert.tofloat(datatable.getvalue(buffer.get('props'),'width')))}
{buffer.set('orgy',convert.tofloat(datatable.getvalue(buffer.get('props'),'height')))}
{buffer.set('newx',convert.tofloat(translation.arg(2,default=0)))}
{buffer.set('newy',convert.tofloat(translation.arg(3,default=0)))}
{buffer.set('ct',translation.arg(4,default=2,rem='1:left
or top, 2:center, 3:right or bottom'))}

<!--// Determine if we will crop vertical or horizontal-->

{buffer.set('vertical',(buffer.get('newy')/buffer.get('orgy')) LT (buffer.get('newx')/buffer.get('orgx')))}

 

<!--// Determine the cropfactor. This is used to calculate the left/top cropvalue based on the croptype given -->

{buffer.set('cf',sys.iif(buffer.get('ct')==1,0,sys.iif(buffer.get('ct')==2,0.5,1)))}

<!--//

The main distinction between images is whether it needs to be made smaller or made bigger.

Depending on this the determination of vertical or horizontal cropping is exactly opposite.

-->

<se:if expression="buffer.get('newx') LT buffer.get('orgx', default=0) OR buffer.get('newy') LT buffer.get('orgy',default=0)">
  <se:then>

    <!--// Depending on the vertical parameter, the width or height cropping parameters are added to the querystring.-->

    <se:if expression="buffer.get('vertical')">
      <se:then>
        {buffer.set('crop',buffer.get('orgy') - ((buffer.get('newy') * (buffer.get('orgx') / buffer.get('newx')))))}
        {buffer.set('cropval1',math.round(buffer.get('crop')
* buffer.get('cf')))}
        {buffer.set('cropval2',math.round(buffer.get('orgy')
- (buffer.get('crop'))))}
        {buffer.set('imgparms','hid=img;rm=17;w='
+ buffer.get('newx') +';crh=' + buffer.get('cropval2') + ';crt=' + buffer.get('cropval1') + ';q=100;ar=1;o=0')}
      </se:then>
      <se:else>
        {buffer.set('crop',buffer.get('orgx') - ((buffer.get('newx') * (buffer.get('orgy') / buffer.get('newy')))))}
        {buffer.set('cropval1',math.round(buffer.get('crop')
* buffer.get('cf')))}
        {buffer.set('cropval2',math.round(buffer.get('orgx')
- (buffer.get('crop'))))}
        {buffer.set('imgparms','hid=img;rm=17;h='
+ buffer.get('newy') + ';crw=' + buffer.get('cropval2') + ';crl=' + buffer.get('cropval1') + ';q=100;ar=1;o=0')}
      </se:else>
    </se:if>
  </se:then>
  <se:else>
    <!--// Depending on the vertical parameter, the width or height cropping parameters are added to the querystring.-->

    <se:if expression="!buffer.get('vertical')">
      <se:then>
        {buffer.set('crop',buffer.get('orgx') - ((buffer.get('newx') * (buffer.get('orgy') / buffer.get('newy')))))}
        {buffer.set('cropval1',math.round(buffer.get('crop')
* buffer.get('cf')))}
        {buffer.set('cropval2',math.round(buffer.get('orgx')
- (buffer.get('crop'))))}
        {buffer.set('imgparms','hid=img;rm=17;h='
+ buffer.get('newy') + ';crw=' + buffer.get('cropval2') + ';crl=' + buffer.get('cropval1') + ';q=100;ar=1;o=0')}
      </se:then>
      <se:else>
        {buffer.set('crop',buffer.get('orgy') - ((buffer.get('newy') * (buffer.get('orgx') / buffer.get('newx')))))}
        {buffer.set('cropval1',math.round(buffer.get('crop')
* buffer.get('cf')))}
        {buffer.set('cropval2',math.round(buffer.get('orgy')
- (buffer.get('crop'))))}
        {buffer.set('imgparms','hid=img;rm=17;w='
+ buffer.get('newx') +';crh=' + buffer.get('cropval2') + ';crt=' + buffer.get('cropval1') + ';q=100;ar=1;o=0')}
      </se:else>
    </se:if>
  </se:else>
</
se:if>

    <!--// Build the final Image Server querystring and return it as a result of the translation. It checks if the given url already had a querystring or not. -->

{translation.setresult(translation.arg(1) + sys.iif(string.indexof(translation.arg(1),'?') GT -1,char.amp(),'?') + buffer.get('imgparms')


 

Share:

Sandor Voordes schreef

Comments (0)

Sandor Voordes

Let's perform

20

Feb

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

Sandor Voordes schreef

Comments (0)

Sandor Voordes

Zoeken

Categorie

Archief


Sign In