Generate CSS sprites and thumbnail images on the fly in ASP.NET sites - part 1

by Matt Perdeck 14. September 2011 20:49

This Series

Introduction

On most web pages, images make up most of the files loaded by the browser to show the page. As a result, you can significantly reduce bandwidth costs and page load times by compressing large images, properly resizing thumbnails and combining small images into CSS sprites to cut down on request/response overhead. This is especially important if your site caters to iPhone users and other smart phones, because of low download speeds, high network latencies and high bandwidth costs for your visitors.

However, doing all this image manipulation manually is a mundane task that takes a lot of time. As a result, it often doesn't get done due to time pressures, or simple lack of awareness.

It was to solve this problem that this ASP.NET CSS Sprite Generator package was born. Once set up, it does all the compressing, resizing and sprite generation for you. The compressed and/or resized image versions and the sprites are generated on the fly into a separate directory. This means that if you add an image to a page on your live site, it will pick that up, with no need to go through a separate build process. However, you still have full control over which images get compressed, resized or combined into CSS sprites through configuration in your web.config.

Adding the ASP.NET CSS Sprite Generator package to an existing site is easy - just add a single dll and a few lines in the web.config - no need to change the site itself. This sets it apart from its main counterpart, Microsoft's ASP.NET Sprite and Image Optimization Library, which requires that you move images to a special directory, replace img tags with special user controls or MVC helpers, etc.

On the fly processing of images inherently puts a greater load on the web server CPU. To overcome this, the package uses caching extensively, so once a sprite has been generated or an image compressed or resized, the package won't have to do this again unless the constituent files are changed or the generated files removed. Also, it uses image processing algorithms that have been optimized for speed.

To make it easy to get started, there is a Quick Start section right after the Installation section. The "out of the box" default configuration means you'll reduce bandwidth and page load times even without configuring the package. And the solution in the download contains 10 simple but fully functional demo sites that show various features of the package.

Contents

Because CSS sprite generation is a surprisingly involved subject, this article is split into 3 parts.

Part 1

Part 2

Part 3

Requirements

  • ASP.NET 4 or higher
  • IIS 6 or higher for your live site

This package can run in shared hosting accounts that only provide Medium trust, such as those provided by GoDaddy. It can also run in accounts that provide Full trust.

Features compared with the ASP.NET Sprite and Image Optimization Library

This section compares the ASP.NET CSS Sprite Generator package against the other major package that generates sprites on the fly for ASP.NET sites, Microsoft's ASP.NET Sprite and Image Optimization Library.

The biggest difference between the two packages is that the ASP.NET CSS Sprite Generator package is much easier to install and configure:

  • With the ASP.NET Sprite and Image Optimization Library, you have to move all images that you want combined into sprites to a separate App_Sprites directory. You also have to manually replace the img tags of those images with special user controls (for ASP.NET sites) or special helpers (for ASP.NET MVC sites). Additional configuration involves adding special settings.xml files.

  • With the ASP.NET CSS Sprite Generator package, you just add a few lines to your web.config - no need to move images or change your pages.

Below is a more detailed feature comparison of the ASP.NET CSS Sprite Generator package and the ASP.NET Sprite and Image Optimization Library:

CSS Sprite
Generator package
ASP.NET Sprite and Image
Optimization Library
Combines images (except animated images) into sprites on the fly. When you add an image to a page, it will be picked up, without requiring additional build steps.
Processes dynamically generated image tags, such as from a database.
Images can optionally be combined based on file type, width, height and other properties (details).
Allows you to combine all images in a specific directory into sprites that are shared amongst your pages, to maximise browser caching (details).
Uses sophisticated algorithms to generate the smallest possible sprites.
Processes .png, .gif and .jpg images.
Processes CSS background images. Caters for repeating background images and background images used with the sliding door technique (such as used with flexible buttons and tabs) (details).
Very easy to install - just add a .dll to your site and add a few lines to your web.config (installation instructions). All configuration is done in your web.config. No need to reorganize your images or to use special controls.
Works seamlessly with the Combine And Minify package. Adding this (free) package further reduces page load times by combining and minifying CSS and JavaScript files, using cookieless domains, far future caching with file versioning, etc.
Allows you to switch features on or off per page or per group of pages (details).
Lets you compress .png and .gif files on the fly by reducing their color depth. Choose one of 6 built in algorithms or use the default (details).
Lets you compress .jpg files on the fly by reducing their image quality (details).
Lets you physically resize images on the fly (for example for thumbnails), either via configuration in web.config (details) or by setting width and/or height attributes on your img tags (details).
Lets you set the maximum file size for sprites, so sprites that grow too big are automatically split into smaller sprites (details).
Automatically replaces img tags with the HTML required to correctly show the generated sprites (details). Copies attributes such as id and onclick from your img tags to the generated html (configurable).
The additional generated CSS required to correctly show the generated sprites (details) can be inlined or automatically placed in a separate CSS file (details).
Can be used with sites hosted on low cost shared hosting plans, such as GoDaddy's (details).
Allows you to configure the package so it only kicks in in Release mode. That way, you see your individual unprocessed images while developing, and reap the performance improvement in your live site (details).
Has the option to throw an exception when an image is missing, so you quickly detect missing images (details).
Generates inlined images
To reduce CPU overhead and disk accesses caused by the package, caches intermediate results. Cache entries are removed when the underlying file is changed, so you'll never serve outdated files.

The features shown above can all be switched on and off individually in the ASP.NET CSS Sprite Generator package via the web.config file (full description). If you just install the package and not do any further configuration, it combines all .png and .gif images (except animated images) that are no more than 100px wide and high into sprites - which is most commonly what you want. By default, it only kicks in when the site is in Release mode (details).

This package is just one way of improving the performance of your web site. My recently released book ASP.NET Performance Secrets shows how to pinpoint the biggest performance bottlenecks in your web site, using various tools and performance counters built into Windows, IIS and SQL Server. It then shows how to fix those bottlenecks. It covers all environments used by a web site - the web server, the database server, and the browser. The book is extremely hands on - the aim is to improve web site performance today, without wading through a lot of theory first.

Introduction to CSS Sprites

What are CSS sprites

Web pages often contain many small images, which all have to be requested by the browser individually and then served up by the server. You can save bandwidth and page load time by combining these small images into a single slightly larger image, for these reasons:

  • Each file request going from the browser to the server incurs a fixed overhead imposed by the network protocols. That overhead can be significant for small image files. Sending fewer files means less request/response overhead.
  • Each image file carries some housekeeping overhead within the file itself. Combining small images gives you a combined image that is a lot smaller than the sum of the individual images.

Take for example this HTML:

<img src="contactus.png" width="50" height="50" />
<img src="cart.png" width="50" height="50" />
<img src="print.png" width="46" height="50" />

Which loads these images:

cart.png contactus.png print.png
Size: 2.21 KB Size: 2.02KB Size: 2.05KB Total Size: 6.28KB
50px x 50px 50px x 50px 46px x 50px

Combining these images gives you this combined image:

combined.png
Size: 3.94KB
146px x 50px

At 3.94KB, the size of this combined image is only 63% of the combined size of the three individual images.

Showing the individual images

Although the three images have now been combined into one single image, they can still be shown as individual images using a CSS technique called CSS Sprites. This involves replacing your img tags with the following div tags (done for you by the package):

<div style=
  "width: 50px; height: 50px; background: url(combined.png) -0px -0px; display:inline-block;"
  ></div>
<div style=
  "width: 50px; height: 50px; background: url(combined.png) -50px -0px; display:inline-block;"
  ></div>
<div style=
  "width: 46px; height: 50px; background: url(combined.png) -100px -0px; display:inline-block;"
  ></div>

Instead of using img tags, this HTML uses div tags with combined.png as the background image.

The CSS background property lets you specify an offset into the background image from where you want to show the background image as background. That is used here to slide the combined image over each div tag, such that the correct area is visible:

  • Here, the cart image sits right at the left edge horizontally and right at the top vertically within the combined image, so it gets offset 0px 0px.
  • The contactus image sits 50px away from the left edge, and right at the top vertically, so it gets -50px 0px.
  • And finally the print image sits 100px away from the left edge, and right at the top vertically, so it gets -100px 0px.

Note that the div tags have a width and height equal to the width and height of the original individual images. That way, you only see the portions of the combined image that correspond with the individual images.

Finally, the div tags use display:inline-block; to further mimick the behaviour of img tags. By default, when you place a number of img tags on a page, they appear horizontally after each other. div tags however by default sit below each other. The display:inline-block; causes the div tags to sit after each other, just like img tags.

Although the additional CSS looks big, it is highly compressible because it has lots of repeating bits of text. So as long as your web server has compression switched on (details) the overhead of the inlined CSS should be minimal.

Additionally, instead of having the additional CSS inlined in the div tags, you can put this in an external .css file and use CSS classes to link the div tags with their CSS. The package supports both options via the inlineSpriteStyles property.

In case you're wondering about writing all this CSS and HTML, this is all done for you by the package. This section simply shows how it's done.

Accessibility and the alt attribute

The alt attribute of the img tag is used to describe the image in words. This enables vision impaired people who use text-only browsers to find out what the image is about. For example:

<img src="cart.png" alt="Shopping Cart" />

The div tag doesn't have an alt attribute. The solution is to simply use the image description as the contents of the div tag:

<div style="...">Shopping Cart</div>

However, the text now sits visibly on top of the background image. To make it invisible, you can apply a large negative indent to shift it out of the area bounded by the div tag:

<div style="text-indent:-9999px; ...">Shopping Cart</div>

If there is no alt attribute or if it is empty, you can't leave the div tag without content, because that will cause the browser to wrongly position the tag. In that case, use &nbsp; as a filler:

<div style="text-indent:-9999px; ...">&nbsp;</div>

Linked images

A case apart is images that are part of a hyperlink, such as:

<a href="Cart.aspx"><img src="images/cart.png" alt="Shopping Cart" /></a>

The first issue is that strictly speaking, in HTML you can't have a block level element such as div inside an inline element such as a. That's easily solved by using a span instead of a div tag:

<a href="Cart.aspx"><span style="..." >Shopping Cart</span></a>

Another issue is that although the text within the span will be indented out, many browsers still show the underlining for the hyperlink. That can be fixed by setting the text-decoration of the anchor to none:

<a href="Cart.aspx" style="text-decoration: none;" ><span style="..." >Shopping Cart</span></a>

A final problem is that instead of the entire image being clickable, only the area taken by the text within the span is now clickable. The anchor doesn't regard a background image as real content. The solution is to turn the anchor itself into a block level element with the same dimension as the image itself. Also move the background image from the span to the anchor:

<a href="Cart.aspx" style=
  "text-decoration: none; width: 50px; height: 50px; 
    background: url(combined.png) -0px -0px; display:inline-block;" 
  ><span style="display:inline-block;text-indent:-9999px;" >Shopping Cart</span></a>

Keep your sprites small

Combining images into sprites tends to only make sense for small images, such as icons and background images.

With bigger images, the request/response overhead is not significant, taking away the reason to combine them into sprites. Additionally, you might wind up with very big sprites. Now that most browsers in use today load at least 6 images concurrently, you may actually be better off having a couple of medium sized images rather than one big sprite.

To manage sprite sizes, you can determine the maximum width, height and file size of the images that will be combined. You can also set a maximum sprite size - so sprites that grow too big get split in two.

Using Shared Hosting

Your site may be using a shared hosting plan, which means that it shares a web server with many other web site. Many companies that offer these plans, such as GoDaddy, restrict the operations that each web site can perform so they don't affect each other.

In technical terms, this means your live site has trust level Medium. In your development environment it has has trust level Full, meaning it can do whatever it wants.

The package was specifically build so it can run with trust level Medium, so you should have no problem using it with your site in a shared hosting plan.

The only issue is that the sprites generated by the package may have somewhat greater file sizes in trust level Medium than in trust level Full. This is because some of the algorithms required to reduce the number of colors in an image are not available in trust level Medium. More details here.

Combine And Minify

Compressing/resizing images and combining them into sprites is a good way to reduce bandwidth and page load times, but there is a lot more that can be done. This includes combining and minifying CSS and JavaScript files, the use of cookieless domains, far future caching, etc.

To take care of this, the package can be installed side by side with another package, Combine And Minify, which has these features:

  • Minifies JavaScript and CSS files. Minification involves stripping superfluous white space and comments.

  • Combines JavaScript files and CSS files to save request/response overhead.

  • Processes .axd files as used by the ASP.NET AJAX toolkit.

  • Removes white space and comments from the HTML generated by your .aspx pages.

  • Makes it easy to use cookieless domains with CSS, JavaScript and image files, to stop the browser from sending cookies with those files.

  • Lets you configure multiple cookieless domains. This causes the browser to load more JavaScript, CSS and image files in parallel.

  • Optimizes use of the browser cache by allowing the browser to store JavaScript, CSS and image and font files for up to a year. Inserts version ids in file names to ensure the browser picks up new versions of your files right away, so visitors never see outdated files.

  • Preloads images immediately when the page starts loading, instead of when the browser gets round to loading the image tags - so your images appear quicker. You can give specific images priority.

The demo site DemoSite_CombineAndMinify in the downloaded solution demonstrates the Combine And Minify package and this package working together. For details, read the notes on the home page of DemoSite_CombineAndMinify.

There are no special requirements or installation steps when using this package together with the Combine And Minify package. Simply go throught the installation steps of each package (pretty simple) and your site will be using both.

Installation

Take these steps to add the ASP.NET CSS Sprite Generator package to your web site:

  1. Compile the package:
    1. Download the zip file with the source code, and unzip in a directory.
    2. Open the CssSpriteGenerator.sln file in Visual Studio 2010 or later.
    3. You'll find that the sources are organized in a solution, with these elements:
      1. Project CssSpriteGenerator is the actual package.
      2. A number of demo sites with examples of the use of the package. You can ignore these if you want.
    4. Compile the solution. This will produce a CssSpriteGenerator.dll file in the CssSpriteGenerator\bin folder.

  2. Update your web site:
    1. Add a reference to CssSpriteGenerator.dll to your web site (in Visual Studio, right click your web site, choose Add Reference).
    2. Add the custom section cssSpriteGenerator to the configSections section of your web.config:
      <configuration>
          ...
          <configSections>
              ...
              <section 
                  name="cssSpriteGenerator" 
                  type="CssSpriteGenerator.ConfigSection" 
                  requirePermission="false"/>
              ...
          </configSections>
          ...
      </configuration>
    3. Add a folder called App_Browsers to the root folder of your web site: Right click your web site in Visual Studio, choose Add ASP.NET Folder, choose App_Browsers.
    4. Use your favorite text editor (such as Notepad) to create a text file in the App_Browsers folder. Call it PageAdapter.browser. Into that file, copy and paste this code:
      <browsers>
          <browser refID="Default">
              <controlAdapters>
                  <adapter controlType="System.Web.UI.Page"
                     adapterType="CssSpriteGenerator.CSSSpriteGenerator_PageAdapter" />
              </controlAdapters>
          </browser>
      </browsers>
      This tells ASP.NET to leave processing of the page to the class CssSpriteGenerator.CSSSpriteGenerator_PageAdapter (which is part of the package).

  3. The package is now installed in your web site. By default, it is only active when your site is in Release mode (debug="false" in your web.config). To make it active in Debug mode as well, add a cssSpriteGenerator element to your web.config, after the configSections section:

    <configuration>
        ...
        <configSections>
            ...
            <section 
                name="cssSpriteGenerator" 
                type="CssSpriteGenerator.ConfigSection" 
                requirePermission="false"/>
            ...
        </configSections>
    
        <cssSpriteGenerator active="Always">
        </cssSpriteGenerator>
        ...
    </configuration>

Quick Start

The full story about all the options and how to configure them is in the configuration section. But there are many options, so this section introduces you to the ones that may be most relevant to you, to get you started quickly.

The downloaded solution contains a series of demo sites with working examples. The demo site DemoSite_QuickStart was set up to make it easy to follow this section - apply the examples further on in this section to its web.config and than run the site to see how the changes leads to different sprites being generated.

This section looks at these scenarios:

Default configuration

If you just install the package and not do any configuration, than it will combine all .png and .gif images on the page with width and height less than 100px into sprites. But it only works when the site is in Release mode.

To give it a try:

  • Make sure that debug is set to false in your web.config.
  • Open a page on your site that has multiple .png or .gif images with width and height less than 100px.
  • View the source of the page to see the html for the sprite.
  • You will find a new directory ___spritegen in the root of your site with the generated sprite.

Using the package in Debug mode

To use the package in both Debug mode and Release mode, you need to do your first bit of configuration. You do that by adding a cssSpriteGenerator element to your web.config file, within the configuration section:

<configuration>
    ...
    <cssSpriteGenerator>
    </cssSpriteGenerator>
    ...
</configuration>

Than set the active property to Always to make the package active in both Debug and Release mode:

<configuration>
    ...
    <cssSpriteGenerator active="Always">
    </cssSpriteGenerator>
    ...
</configuration>

Processing bigger images

To increase the maximum width for images to be processed to 200px (from the default 100px) and the maximum height to 300px (from the default 100px), add an imageGroup element, like so:

<configuration>
    ...
    <cssSpriteGenerator active="Always">
        <imageGroups>
            <add maxWidth="200" maxHeight="300"/>
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

Image groups are a fundamental concept in the package. When an image is found on the current page, in a group of background images, or in a directory on the server, it is first added to a group. You can set limitations on the images that can be added to a group, such as maximum width, maximum height, file path, etc. The images in the group are then combined into one or more sprites. If an image doesn't get added to any group, it is simply ignored.

Your site can have multiple groups. The order of your groups is very important. When the package tries to match an image to a group, it starts at the first group and works its way to the last group. The first group that matches is the group that the image is added to. This means you need to list more specific groups before more general groups! In other words, don't write this:

<configuration>
    ...
    <cssSpriteGenerator active="Always">
        <imageGroups>
            <!--WRONG ORDER-->
            <!--matches all images with width 200px or less, irrespective of height-->
            <add maxWidth="200"/>

            <!--intended to match all images 200px wide or less and 300px high or less, 
            but won't match anything, because these images all match the preceding group-->
            <add maxWidth="200" maxHeight="300"/>
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

Instead, put the more specific group first:

<configuration>
    ...
    <cssSpriteGenerator active="Always">
        <imageGroups>
            <!--matches all images 200px wide or less and 300px high or less-->
            <add maxWidth="200" maxHeight="300"/>

            <!--matches all images with width 200px or less and height greater than 300px-->
            <add maxWidth="200"/>
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

So why does the package combine .png and .gif images into sprites if you haven't defined any groups? The reason is that if there are no groups at all, the package internally creates a default group. This default group only allows .png and .gif images, and then only those whose width and height are 100px or less. The moment you add your own groups however, that default group no longer gets created.

Jpg icons

If you have .jpg images on your page that are 200px by 300px or smaller, you will have noticed that they started getting combined into a sprite together with the other images after you added a new group in the previous section.

This is because groups allow all images, except if you explicitly add a limitation. The default group keeps out .jpg images, but your new group doesn't have such a limitation.

To allow only images with a certain file type, directory or name into a group, use the filePathMatch property of the group element. This is a regular expression. Images whose file paths don't match that regular expression are excluded.

To allow into your group only .png and .jpg images that are 200px by 300px or smaller, you would write this:

<configuration>
    ...
    <cssSpriteGenerator active="Always">
        <imageGroups>
            <add maxWidth="200" maxHeight="300" filePathMatch="(gif|png)$"/>
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

Shared sprites

Your site probably has a directory with small images, such as a shopping basket, arrows, etc. Instead of generating sprites per page, it makes sense to combine all those small images into a single sprite that can then be shared by all pages. That way, when your visitor moves to another page on your site, the shared sprite is probably still in their browser cache, so their browser doesn't need to retrieve it from your server.

You can get the package to read images from a folder on the web server with the processFolderImages and imageFolderPathMatch properties. However, those images then still need to match a group in order to get combined into a sprite.

Assuming your images sit in the directory images on your web server, you'd write something like this:

<configuration>
    ...
    <cssSpriteGenerator active="Always" 
    processFolderImages="true" imageFolderPathMatch="\\images\\" >
        <imageGroups>
            <add maxWidth="200" maxHeight="300" filePathMatch="(gif|png)$"/>
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

Because in this example the only group only allows .gif and .png images that are 200px by 300px or smaller, any .jpg images or images bigger than 200px by 300px in the images directory won't be processed.

Compressing big .jpg images

Many web sites have large photos that would take a lot less bandwidth if someone compressed them. Compressing images can save a lot of bytes without visible loss of image quality, but often people don't take the time to do it.

You can compress a .jpg image by reducing its quality with the jpegQuality property. Setting it to 70% often has good results. You can make sure that your photos don't get combined into sprites with other images with the giveOwnSprite property (why). Finally, you can ensure that the generated image has the JPG format (instead of the default PNG format) by setting the spriteImageType property.

Let's use this in a group:

<configuration>
    ...
    <cssSpriteGenerator active="Always" >
        <imageGroups>
             <add filePathMatch="\\photos\\" 
             jpegQuality="70" giveOwnSprite="true" spriteImageType="Jpg" />
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

The compressed photos wind up in the ___spritegen directory along with the other generated images. Check their file sizes to see how many bytes you're saving. If you're trying this out on the DemoSite_QuickStart site, you'll find that the file size of the large photo is reduced from 79.3KB to 58.6KB - a 26% saving without visible loss of quality.

Generating thumbnails

Your site may have thumbnails that link to the full sized photos. To produce a thumbnail, you could simply use the original image and use the width and height attributes of the img tag to reduce its size on the page. However, this causes the browser to load the full sized original image file, which is not an optimal use of your bandwidth. You could physically reduce the image in your favorite image editing program, but that gets boring if you have many images.

All this gets a lot simpler after you've installed the ASP.NET CSS Sprite Generator package, because it physically resizes images whose width and height attributes are different from their physical width and height.

Or you could use the resizeWidth and/or resizeHeight property:

<configuration>
    ...
    <cssSpriteGenerator active="Always" >
        <imageGroups>
             <add filePathMatch="\\photos\\" resizeHeight="200" spriteImageType="Jpg" />
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

You probably only want to resize the photos on your thumbnail pages, not on the pages with the original images. You could make this happen with the pageUrlMatch property. For example, if your thumbnail page is called thumbnail.aspx, you'd write:

<configuration>
    ...
    <cssSpriteGenerator active="Always" >
        <imageGroups>
             <add filePathMatch="\\photos\\" resizeHeight="200" 
             pageUrlMatch="thumbnail.aspx" spriteImageType="Jpg" />
        </imageGroups>
    </cssSpriteGenerator>
    ...
</configuration>

Conclusion

This was part 1 of this 3 part series. In part 2 we'll go in-depth into the configuration options of the ASP.NET CSS Sprite Generator package.

Comments (1) -

steve
steve United States
1/16/2012 9:13:59 AM #

thanks for this

Reply

Pingbacks and trackbacks (1)+

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

Books

Book: ASP.NET Site Performance Secrets

ASP.NET Site Performance Secrets

By Matt Perdeck

Details and Purchase

About Matt Perdeck

Matt Perdeck PresentingMatt has written extensively on .Net and client side software development.

more >>