Skip to content

Posts from the ‘Optimization’ Category

21
Aug

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.

25
Feb

Web Page Optimization – Changes I Made for Page Loading Speed

It has been years since I optimized the page loading speed at GoToQuiz. At that time, image sprites and CSS/javascript minification were the state of the art. In the intervening years, though, further progress has been made. Of particular note is the “retirement” of older versions of Internet Explorer, and the emergence of CSS3 and HTML5, which allows developers the ability to further streamline their sites. So as I am in the process of revamping the UI, I’m taking advantage of the latest techniques to boost page loading speed. Be aware, this is nothing revolutionary–this blog post is simply an overview of which changes I’m making, and why.

Migrating pages over to HTML5 allows the markup to be more semantic while taking fewer bytes–a double win.  Using tags such as nav, section and article helps alleviate so-called “div-itis”, allowing cleaner markup and a smaller overall filesize.  HTML5 also follows the maxim of “convention over configuration”, letting you eliminate unnecessary attributes like type=”text/javascript” on script tags, for example.

The adoption of CSS3 has provided many possibilities for creating beautiful visual effects without using image files.  Here is an example: on an early iteration of the design of the various headings on GoToQuiz, I used this sprite combined with CSS to create the rounded color bar appearance.

Read moreRead more

3
Feb

Prefetching Javascript and CSS

Prefetching javascript and CSS files can improve the page load times as users navigate your site.  This creates a better user experience and may even improve your search engine rankings.

I’ll describe an easy prefetch technique useful in situations in which you are reasonably sure the user will navigate from Page A to Page B.  This situation typically involves the user filling out a form.  Chances are high that if a user begins to fill out a form, she will eventually submit it, arriving at Page B.  If you prefetch resources that will be needed by Page B, it’ll load faster when the user navigates to it.  This is a technique I’m using on our lifespan test.

Read moreRead more