Skip to content

Instantly share code, notes, and snippets.

@pmeenan
Last active March 18, 2023 16:48
Show Gist options
  • Save pmeenan/9ff824428212aeeae1868647696bfa71 to your computer and use it in GitHub Desktop.
Save pmeenan/9ff824428212aeeae1868647696bfa71 to your computer and use it in GitHub Desktop.
Testing Priority Hints with WebPageTest

Priority Hints is rolling out to Chrome in the 101 release which is currently available in the Dev/Beta channel of Chrome and available in WebPageTest when using the Chrome Canary browser selection.

To make it easier to experiment with priority hints (particularly for LCP images) without making production changes, I set up a couple of public Cloudflare Workers that can be used dynamically with WebPageTest to inject priority hints into existing pages and to preload arbitrary images when combined with WebPageTest's overrideHost script command.

Injecting Priority Hints

There is a cloudflare worker at hint.perf.workers.dev that will take a CSS selector from the x-hint HTTP header and add fetchpriority=high to any elements in the HTML that match the selector. The easiest way to experiment with this is to use Chrome's dev tools locally, identify the element that hosts the image, right-click on the element and use copy->selector. It may not produce the most efficient and reusable selector but it should usually work.

The worker also removes any loading attributes from the elements that match the selector in order to disable any lazy loading that might be in place.

Here is a sample WebPageTest script that will boost the priority of the hero image for a WordPress blog:

overrideHost  roadtrip.meenan.us  hint.perf.workers.dev
addHeader x-hint: #post-1739 > div > figure:nth-child(2) > img
navigate  https://roadtrip.meenan.us/?p=1739

Injecting a Preload

There is a cloudflare worker at preload.perf.workers.dev that will take an image URL from the x-preload HTTP header and add a <link rel="preload" as="image" fetchpriority="high" ...> tag to the HTML just before the close of the <head> tag which should be the optimal place in most conditions to preload an image so it loads after any blocking scripts. This can be useful for testing high-priority preloads for background images or JS-injected images.

Here is a sample WebPageTest script that will preload the hero image for a WordPress blog:

overrideHost  roadtrip.meenan.us  preload.perf.workers.dev
addHeader x-preload: https://roadtrip.meenan.us/wp-content/uploads/2020/08/img_6217.jpg
navigate  https://roadtrip.meenan.us/?p=1739

Getting a Clean Baseline

Rewriting pages to go through the Cloudflare worker changes the performance even if no other changes are made. In order to get a clean baseline for comparison, you should test with the same script you are using for the experiment but with the Chrome command-line flag (in advanced settings) of --disable-blink-features=PriorityHints. You could also just remove the addHeader command from the script but disabling the Chrome feature will give you the cleanest baseline.

Putting it all Together

Here is a comparison of both of the sample scripts above along with a comparison baseline. Both scripts had a similar impact, dropping LCP from 2.2 seconds down to 1.3 seconds for the desktop site on a Cable connection.

The filmstrip also demonstrates the tradeoffs involved with the image loading sooner but the custom-styled text loading later. This could be significantly improved by self-hosting the fonts instead of using Google fonts and by preloading the custom font used for the heading.

The Worker Scripts

For completeness (and as an example), here are the worker scripts used for both.

hint.perf.workers.dev:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
});

async function handleRequest(request) {
  const url = new URL(request.url);
  const host = request.headers.get('x-host');
  if(!host) {
    return new Response('x-host header missing', {status: 403});
  }
  url.hostname = host;
  const selector = request.headers.get('x-hint');
  const acceptHeader = request.headers.get('accept');

  if(selector !== null && (acceptHeader !== null && acceptHeader.indexOf('text/html') >= 0)) {
    const response = await fetch(url.toString(), request);

    return new HTMLRewriter()
      .on(selector.trim(), new prioritizeHigh())
      .transform(response);
  }

  // Otherwise just proxy the request
  return fetch(url.toString(), request)
}

class prioritizeHigh {
  element(element) {
    element.removeAttribute('loading');
    element.setAttribute('fetchpriority', 'high');
  }
}

preload.perf.workers.dev:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
});

async function handleRequest(request) {
  const url = new URL(request.url);
  const host = request.headers.get('x-host');
  if(!host) {
    return new Response('x-host header missing', {status: 403});
  }
  url.hostname = host;
  const preloadUrl = request.headers.get('x-preload');
  const acceptHeader = request.headers.get('accept');

  if(preloadUrl !== null && (acceptHeader !== null && acceptHeader.indexOf('text/html') >= 0)) {
    const response = await fetch(url.toString(), request);

    return new HTMLRewriter()
      .on('head', new Preload(preloadUrl))
      .transform(response);
  }

  // Otherwise just proxy the request
  return fetch(url.toString(), request)
}

class Preload {
  constructor(preloadUrl) {
    this.preloadUrl = preloadUrl;
  }
  element(element) {
    const insertHTML = '\n<link rel="preload" href="' + this.preloadUrl + '" as="image" fetchpriority="high">\n';
    element.append(insertHTML, {html: true});
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment