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.
Combine and cache multiple CSS files for performance
This is a simple way to get a performance boost if you’re using PHP + Apache. I am using it to boost performance on GoToQuiz. If you are not using Apache, you can still use the PHP portion of this performance enhancement.
Some quick background: maintaining your site’s CSS in multiple files is frequently useful and sometimes necessary. But including multiple CSS files results in a performance hit when your pages load, because each one requires a separate request from the user’s browser to your server. Each request has associated overhead, which can be minimized if you send all CSS in one request.
This is how you can combine your CSS files with PHP and cache them, relying on a clever bit of .htaccess modification:
Validate that URLs exist using jQuery / PHP
It would be nice to have a pure javascript method of validating that URLs exist. One imagines you could use an AJAX call and verify the HTTP status code (200, 404, etc.) returned. However, browser security does not permit cross-domain AJAX calls. So, this method would only work if you are validating that URLs exist on the same domain.
Perhaps there is a way to use a hidden iframe to test the existence of a URL. I am not aware of a way to get the HTTP status code of a page that loads inside of an iframe, though. I’m not sure it is possible. So you must rely on javascript plus a server-side programming language to perform this validation. I chose jQuery and PHP.
IP Addresses and Geolocation – Using GeoIP
Did you ever wonder how some sites are able to tailor a message specifically to your city? You might have seen these in ads: “Albuquerque mom loses 50 lbs!” Hmm. How did the site know you live in Albuquerque?
The answer is geolocation based on the user’s IP address. And of course, geolocation can be useful beyond tailoring custom ad messages. The good news? Free geolocation functionality exists!