HOW TO: Support .webp on Cloudflare's edge with a Worker

April 4, 2020

Webp is a super fast, lightweight image format that Google Pagespeed likes a lot. Unfortunately, some browsers still don't support it - most notably - Safari.

We need a method to serve .webp when it's requested by browsers that support it, and fall back to .jpg/.png for browsers that do not. Using browser/user agent-based detection could work...

But we need a method that doesn't go wonky when a cache gets involved.

Why would caching make things wonky?

Using a traditional method of browser detection, loading a webpage with Safari will correctly load the .jpg version of an image. However, if a page cache is involved on the server (or in something like Cloudflare), that .jpg then be served to all other browsers that actually support .webp until it expires.

What's worse is if it goes the other way around and the .webp image is cached. While all Chrome and Firefox visitors would be served their images nice and fast - Safari visitors will just see broken/blank spaces where images should be.

What's the solution then?

Assuming we're going to be using page caching (which we should in most cases), we'll settle on using the same file extension (.jpg or .png) no matter what browser loads requests it - and then do some fiddling with the response header to dictate which version of the image to load.

This way, the page cache can always send the same thing, with the browser in tandem with a Cloudflare worker handling the logic of which version of an image should load.

The Cloudflare Worker method:

This method requires that a .webp equivalent of your .jpg or .png files exists in the same directory (ie. /wp-content/uploads/20/04/image.jpg and /wp-content/uploads/20/04/image.webp).

Here's the Cloudflare worker script (kudos to vidaXL):

 * Assume we have an image server able to serve webp, convert png and jpg
 * urls to webp urls if the browser supports it
addEventListener('fetch', event => {

async function makeWebp(request) {

    let regex = /\.jpg$|\.png$/

        && request.headers.get('Accept').match(/image\/webp/)
        && request.url.match(regex)) {
         * Replace jpg / png with webp
        let url = new URL(request.url.replace(regex, '.webp'))

         * Create a new request with the webp url
        const modifiedRequest = new Request(url, {
            method: request.method,
            headers: request.headers

         * Fetch the webp response
        const webpResponse = await fetch(modifiedRequest)

         * Add webworker header to the webp response so we can
         * check live if the webworking is doing what it should do
        const webpHeaders = new Headers(webpResponse.headers)
        webpHeaders.append('X-WebWorker', 'active')

         * Return a new response object
        return new Response(webpResponse.body, {
            status: webpResponse.status,
            statusText: webpResponse.statusText,
            headers: webpHeaders

    } else {
        const response = await fetch(request)
        return response

In my case, I added the above worker to the route* as that's where my WordPress backend is for (which is a headless frontend built on Frontity).

If you're using a traditional WordPress setup, just add* as the route.

To verify that everything is working as it should, right-click a random image and open it in a new tab while inspecting the Network in your browser. You should see that while the image still has .jpg or .png as it's extension but the content-type is image/webp:

Working with WordPress

If you don't already have .webp equivalents of your images and you're using WordPress - the amazing Webp Express plugin has a great bulk convert option.

Use the following settings:

Once those are set, click the Bulk Convert option and go make a cup of tea - it can take a while. Soon, you'll have .webp equivalents of all your uploaded images ready to serve all your visitors and bump your Google Pagespeed Insights score while you're at it.