Skip to content

August 21, 2017

Load Many Images in a Single Request (Optimization)

Some pages on GoToQuiz.com generate many requests to retrieve many thumbnail-sized images. Each image file is only 1 – 5 KB in size. I wanted to reduce the penalty associated with making numerous requests, so I came up with a way to combine these into a single request.

I needed a solution to retrieve an arbitrary number of images, which may vary with each pageview. I considered generating an image sprite on the fly, but this approach came with several problems:

  • Constantly invoking image functions on the server, adding more overhead
  • Image display would require using background-image and CSS manipulation
  • Image display would also depend on meta data about each image, to correctly set the necessary CSS values
  • Images may be different heights and widths, further complicating sprite generation

So instead, I went with returning the image data base64-encoded in a JSON format. It works like this. The HTML will have elements each holding the URI of the image as a data element rather than in <img src=”…”>.

<div class="gImg imgSwapHiPri" data-imgpath="/upic/2017/21/Mn8rrw.T1.jpg"></div>

In this case, it’s a certain kind of thumbnail styled with .gImg to have a specific height, width, and generic default background. The .imgSwapHiPri class is used to tag all elements to have their images swapped in.

And here’s the jquery plugin I wrote, to be called on pageload:

(function($, dataAttr) {

    $.lazyImage = function($swaps, alt) {
        $swaps = $($swaps);

        var thumbs = $swaps.map(function() { 
                return $(this).data(dataAttr);
             }).get();

        if (thumbs.length !== 0) {
            $.get('/img/image-set.json', {"paths[]": thumbs}, function(json) {
                $swaps.each(function(i, el) {
                    if (el.tagName === 'IMG')
                        el.src = json.base64[i];
                    else
                        el.innerHTML = '<img src="'+ json.base64[i] +'" alt="'+ (el.getAttribute('data-alt') || alt || '') +'">';
                });
            });
        }
    };
        
})(jQuery, 'imgpath');

Very simple. Invoked with $.lazyImage(‘.imgSwapHiPri’);. You can pass an optional alt attribute value, or optionally set a data-alt attribute per element. This script checks if the element to be swapped is an IMG tag; if so, set it’s src attribute. If not, set its innerHTML to an IMG tag. Either way, the image data is set to be the base64-encoded image returned by /img/image-set.json.

Let’s take a look at what’s happening in /img/image-set.json’s PHP code. I’ve got some framework boilerplate in the actual script, but the main bit is this:

const WEB_ROOT = '/path/to/public_html/';
$paths = $_POST['paths'];
$b64 = array();

//fill an array with the base64-encoded images        
foreach ($paths AS $path)
    $b64[] = base64EncodeImg($path);

header('Content-type: application/json');

echo '{"base64":';
echo json_encode($b64);
echo '}';

//function to convert image path to base64 data
function base64EncodeImg($path) {
    //first ensure the image path contains only legal characters
    if (preg_match('@^([-_/a-z0-9]+\.)+(png|jpg|gif|svg)$@i', $path)) {
        //absolute path to file
        $path =  WEB_ROOT . (strpos($path, '/') === 0 ? substr($path, 1) : $path);

        if (file_exists($path)) {
            $type = pathinfo($path, PATHINFO_EXTENSION);
            $data = file_get_contents($path);
            return 'data:image/' . strtolower($type) . ';base64,' . base64_encode($data);
        }
        else 
            return '';
    }
    else
        return '';
}

The data generated by the code above is a simple JSON file containing an array of base64-encoded images. Critically, it is generated in the same order as the elements in the HTML. This allows the jquery plugin to easily match base64 data with the appropriate element just by iterating over them.

And that’s really all there is to it. When the pageload event fires, the jquery plugin is called and makes the request for, say, a dozen images. The image data is returned in one JSON response–typically under 30KB in my case with JSON gzipping turned on (mod_deflate)–and it is fast.

BONUS:

If you have alternate high resolution versions of images for high DPI screens, this modified version of the above jquery plugin has your back.

(function($, dataAttr) {
 
    $.lazyImage = function($swaps, alt) {
        $swaps = $($swaps);

        var hdpi = window.devicePixelRatio || 1 > 1.3,
            thumbs = $swaps.map(function() { 
                var $this = $(this);
                return hdpi ? $this.data(dataAttr + 'H') || $this.data(dataAttr) : $this.data(dataAttr);
             }).get();

        if (thumbs.length !== 0) {
            $.get('/img/image-set.json', {"paths[]": thumbs}, function(json) {
                $swaps.each(function(i, el) {
                    if (el.tagName === 'IMG')
                        el.src = json.base64[i];
                    else
                        el.innerHTML = '<img src="'+ json.base64[i] +'" alt="'+ (el.getAttribute('data-alt') || alt || '') +'">';
                });
            });
        }
    };
        
})(jQuery, 'imgpath');

In this case, the plugin checks if the screen is high DPI and checks each element to see if there is a data attribute called “data-imgpath-h”. If so, it uses this instead of the value of “data-imgpath”. The value of data-imgpath-h is the path to the high resolution version of the image.

Share your thoughts, post a comment.

(required)
(required)

Note: HTML is allowed. Your email address will never be published.

Subscribe to comments