Image handling done right

If you are familiar with Enonic CMS, you may already know the power that lies in the image handling functionality available through the XSLT function portal:createImageUrl(). This function lets you scale and manipulate an image in numerous ways. There are, however, great potential pitfalls if you don't use this image handling function with caution.

Since Enonic CMS 4.4, a set of prescaled versions are created for all images uploaded to the CMS. These include "small" (256 pixels width), "medium" (512 pixels width) and "large" (1024 pixels width). In 4.7.4, an "extra large" (2048 pixels width) size was also added. The original version is of course also stored in the CMS, labeled "source".

Through numerous QA processes, consultants at Enonic have discovered that developers very often use the createImageUrl function directly on the source image, not on the prescaled versions that are created when the image is uploaded to the CMS. Depending on the image size, this may result in high system load on the server, and longer page render times for the user.

To illustrate the problem, we have performed the following tests, which compares using createImageUrl directly, versus using the more "sophisticated" stk:image.create in the Enonic STK. The test is performed with the following XSL template:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet exclude-result-prefixes="#all"  version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:portal="http://www.enonic.com/cms/xslt/portal" xmlns:stk="http://www.enonic.com/cms/xslt/stk">

    <xsl:import href="/modules/library-stk/stk-variables.xsl"/>
    <xsl:import href="/modules/library-stk/image.xsl"/>

    <xsl:variable name="scale-width" as="xs:integer" select="1000"/>

    <xsl:template match="/">
        <xsl:call-template name="stk:image.create">
            <xsl:with-param name="image" select="/result/contents/content"/>
            <xsl:with-param name="scaling" select="concat('scalewidth(', $scale-width, ')')"/>
            <xsl:with-param name="scale-up" select="false()"/>
            <xsl:with-param name="filter" select="''"/>
            <xsl:with-param name="quality" select="85"/>
            <xsl:with-param name="format" select="'png'"/>
        </xsl:call-template>
        <img src="{portal:createImageUrl(/result/contents/content/@key, concat('scalewidth(', $scale-width, ')'))}" alt=""/>
    </xsl:template>

</xsl:stylesheet>

Test 1

In the first test we are scaling an image, which originally is 1920 x 1200 pixels big, down to 50 pixel width (thumbnail size).
Below is a screenshot of the network panel in the Chrome Developer tools, which lets you inspect resources that are downloaded over the network. Here we can see that the page contains two images (small.png and 105.png), which both are about 5.2 KB. Pay attention to the load time for the two. While "small.png", which is outputted by stk:image.create, is loaded in 20 ms, the output from createImageUrl ("105.png") is loaded in 423 ms, about 21 times longer. The high load time on "105.png" is due to the server scaling the image based on the original image, which is a lot more CPU intensive than scaling the image based on the closest pre-scaled version.

2 megapixels scaled to 50

2 megapixels scaled to 1000

Test 2

For the second test, a 24 Megapixel image was used (6016 x 4016 pixels), scaled to both 50 pixels and 1000 pixels width. Here we can se that the load time increases substantially, it is over 200 times higher in the first example, and over 5 times higher in the second.

24 megapixels scaled to 50

24 megapixels scaled to 1000

Test 3

For the third test, a 76 Megapixel image was used (10887 x 7044 pixels), scaled to both 50 pixels and 1000 pixels width. Here we even bigger differences than earlier, the load time is 755 times higher in the first example, and over 19 times higher in the second.

76 megapixels scaled to 50

76 megapixels scaled to 1000

PNG or JPEG?

What about the image format? Where do you use PNG and where do you use JPEG? The default image format of createImageUrl is PNG, and we often see that the front end developers don't specify where to use which format. Thus, sometimes every images on the site is in the lossless PNG format, with the consequence that the bandwidth usage shoots through the roof. Images stored as PNG are typically 5-15 times larger than when saved as JPEG. In other words, a 100 KB JPEG typically is about 1 MB stored as PNG.

As a rule of thumb, you would normally only use the PNG format when you need transparency, or in cases where you need the image to be of lossless quality. JPEG is used for everything else, for example article images, staff images, illustrations and so on. The compression factor (and quality) of a JPEG can be tuned by setting the compression factor.

Lesson learned

At first, using createImageUrl directly might seem like the easiest way to handle images, but as we have seen, this might have an extremely negative effect on system performance. Everything might look and perform OK in the test environment, but what happens when tens, hundreds or thousands of images are requested at the same time? Enonic recommends using stk:image.create or stk:image.create-url for best practice image handling.