Moving to WebP Image with Fallback to PNG

Currently, I'm writing my notes of Rust learning, and it usually comes with many screenshots even in a single post. Though PNG file could serve the original / or best quality, the size of which is considerably large. So it takes a few seconds to load despite using Cloudflare CDN.

WebP has been released years ago by Google, and I'd considered that before. However, in the early days, it only works in Google Chrome. Lacking support in other mainstream browsers was a main barrier for me to adopt it.

And now many browsers support WebP image, and WebP format has also been updated in these years. It could serve nearly the original quality but half or even sometimes less than one third the size of corresponding PNG file.

Yet it's surprised to me that Safari hasn't adopted WebP format, which means I have to provide a fallback. Well then, given that I'm also planning to speed up page load by deferring images, the extra thing would be checking WebP support.

To defer image loading, the original <img> tag is

<img src="https://ryza.moe/wp-content/uploads/2019/10/ouo-dangling-pointer.webp" />

And it will be changed into

<img data-src="https://ryza.moe/wp-content/uploads/2019/10/ouo-dangling-pointer.webp" />

But browsers won't load anything if we don't assign URL to src attribute. We need JavaScript to do that.

function defer() {
    var imgDefer = document.getElementsByTagName('img');
    for (var i = 0; i < imgDefer.length; i++) {
        if (imgDefer[i].getAttribute('data-src')) {
            imgDefer[i].setAttribute('src', imgDefer[i].getAttribute('data-src'));
        }
    }
}
window.onload = defer;

As soon as the page finishes loading, this script performs defer() to find all img tags, and then checks the existence of data-src attribute for every img. If it presents, then assign that URL to src attribute.

How about the PNG fallback?

The first thing is to check WebP support. Thanks to https://stackoverflow.com/a/5573422, we have a nice function to do that. And I just made some minor changes, because I always use lossless WebP image, so I removed the test for lossy WebP support.

// thanks to https://stackoverflow.com/a/5573422
var hasWebP = (function() {
    return function() {
        var deferred = $.Deferred();

        $("<img>").on("load", function() {
            // the images should have these dimensions
            if(this.width === 2 && this.height === 1) {
                deferred.resolve();
            } else {
                deferred.reject();
            }
        }).on("error", function() {
            deferred.reject();
        }).attr("src", "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAQAAAAfQ//73v/+BiOh/AAA=");

        return deferred.promise();
    }
})();

hasWebP().then(function() {
    // support lossless WebP!
}, function() {
    // fallback
});

And then assemble them together. Noting that WordPress uses jQuery instead of the default $

function defer() {
    // thanks to https://stackoverflow.com/a/5573422
    var hasWebP = (function() {
        return function() {
            var deferred = jQuery.Deferred();

            jQuery("<img>").on("load", function() {
                // the images should have these dimensions
                if (this.width === 2 && this.height === 1) {
                    deferred.resolve();
                } else {
                    deferred.reject();
                }
            }).on("error", function() {
                deferred.reject();
            }).attr("src", "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAQAAAAfQ//73v/+BiOh/AAA=");

            return deferred.promise();
        }
    })();

    var imgDefer = document.getElementsByTagName('img');
    hasWebP().then(function() {
        // support WebP!
        for (var i = 0; i < imgDefer.length; i++) {
            if (imgDefer[i].getAttribute('data-src')) {
                imgDefer[i].setAttribute('src', imgDefer[i].getAttribute('data-src'));
            }
        }
    }, function() {
        // fallback
        for (var i = 0; i < imgDefer.length; i++) {
            if (imgDefer[i].getAttribute('data-src')) {
                imgDefer[i].setAttribute('src', imgDefer[i].getAttribute('data-src').replace(".webp", ".png"));
            }
        }
    });
}

window.onload = defer;

Finally, write a WordPress plugin PHP to register and enqueue the JavaScript at the footer.

<?php
/*
* Plugin Name: WebP Fallback
* Description: Using .webp in supported browsers with fallback to .png
* Version: 1.0.0
* Author: [data deleted]
* Author URI: https://ryza.moe/
* License: GPLv3
* License URI: http://www.gnu.org/licenses/gpl-3.0.html
*/

class WebPFallback {
    static $add_fallback_script;
    
    public static function init() {
        add_action( 'wp_footer', array( __CLASS__, 'add_script' ) );
    }
    
    public static function add_script() {
        if (!self::$add_fallback_script) {
            wp_enqueue_script( 'webp-fallback', plugins_url('fb.js', __FILE__), false, '1.0.0', false );
            self::$add_fallback_script = true;
        }
    }
};

WebPFallback::init();

Furthermore, we could use pngcrush to optimise PNG file. So the workflow goes as

#!/bin/bash

# do lossless color-type or bit-depth reduction
pngcrush -reduce $1 ${1%.png}.opt.png
# lossless PNG to lossless WebP
cwebp -z 9 -m 6 ${1%.png}.opt.png -o ${1%.png}.webp
# rename
mv $1 ${1%.png}.original.png
mv ${1%.png}.opt.png $1

As seen above, the original PNG file occupied 652KB, and optimised PNG file shrank to 370KB, about 43%! And the WebP file only take 90KB, 86% smaller the original image, and even 75% smaller than optimised one.

4 thoughts on “Moving to WebP Image with Fallback to PNG”

    1. The way that automatically converts JPG / PNG to WebP is awesome! I've considered using plugins like mod_pagespeed, but that would introduce extra tasks on the server.

      And sadly, Safari doesn't support WebP yet and yes, I use Safari as my daily web browser (I got Chrome installed, but just use it occasionally). Because reading list, history, passwords and opened tabs can be automatically synced across all my devices. It's really convenient to me.

      Actually, it's quite easy to backup / migrate WordPress site, just 1) get a copy of /PATH/TO/YOUR/WordPress (maybe zip it as well), 2) export Database using mysqldump -u [username] -p [databaseName] > [filename]-$(date +%F).sql.

      1. Well, for me I personally use Firefox along with Chrome, they have particular functions.
        Firefox is loaded with a bunch of plugins and extensions(Such as RandomUA, Shodan, NoScript and more), which is a browser I've used for almost all the searching and playing, the history and Cookies are of course deleted when exit for this browser.
        Chrome for me is only for maintaining the login status of the trusted websites (such as Cloudflare, Google), and the incognito tab for Chrome is running without any proxies for listening music.
        Passwords are stored locally and synced on change.
        Letting Google know what I'm looking and keep passwords are still seems weird to me at the moment. >_<

        For WordPress, let me try that on my Ignorance Notebook at next time. WordPress's function to transform images into different sizes is very good, but this could introduce different sizes of images stored on disk, which is why I didn't like it, along with the way it stores images (as /yy/mm/dd/xxx.jpg), but, it might be just me own appetite for organizing images(and URLs).

        1. Yes, lack of media file path customisation and generating unused sizes of images are some cons of WordPress.

Leave a Reply

Your email address will not be published. Required fields are marked *

5 × five =