Expand client hints to include preloading support

Tags: #<Tag:0x00007fdf764f79c0>

Client Hints exposes JS equivalent of navigator.connection APIs that help in loading optimizations. But other APIs like preload support detection (DOMTokenListSupports(document.createElement("link").relList, "preload"); ) are also relevant to loading optimizations and can benefit from inclusion in Client Hints.

Example use case: In my server side rendered app, I render inline scripts. These scripts can have different contents based on if client supports preloading. I could add basic defer script loader if preload is not supported or a more sophisticated script loader if preload is supported, where script execution time is controlled by certain events.

While I can sympathize with the desire to avoid sending unused script code, I don’t know that it’s tenable to add a client hint for every possible feature detectable platform feature.

Can you estimate the code size differences between preload-supporting browsers and non-supporting ones? What happens if you send preload-related code to all and perform the feature detection on the client side?

I don’t think we need to send this for every possible feature detectable platform feature. More detailed request:

  1. On initial page load, there are no client hints sent.
  2. Client adds Accept-CH: preload-support header to request (but its pointless since page with inline script is already loaded. So additional header Accept-CH-Lifetime is added with long time period)
  3. Future requests to load the html page (within Accept-CH-Lifetime) will get client hints and can serve better inline scripts.

What distinguishes preload here from other features? Any example on how the scripts would be significantly different if they knew of lack of preload support?

Also note that older browsers that don’t support preload will also not support this new Client Hint.

What distinguishes preload here from other features?

I think eventually many more features should be exposed via Client Hints (when explicitly requested by Accept-CH to avoid bloating header). But preload support allows for many optimizations, specially when apps use server-side rendering.

Any example on how the scripts would be significantly different if they knew of lack of preload support?

Sure, in my server-side rendered app. If preload is not supported, my inline script would be:

const scEl = document.createElement('script');
scEl.src = '<inserted-by-server>';
scEl.defer = true;

In case of preloading, it could be more complex / optimized:

const plEl = document.createElement('link');
plEl.href = '<inserted-by-server>';
plEl.rel = 'preload';
plEl.as = 'script';

Then I can have an event in my app that is triggered by some action, and the event can force-execute the preloaded script (by using the url in script src and appending to head). So it gives control on when the script is executed rather than async or defer. If my app has 500 results, I can execute after first 50 results are loaded. But in summary, it’s a different script loader essentially that leverages preload support to execute the script at the right/intended time.

Seems like client-side feature detection can enable a single script to do both of those things with minimal overhead (e.g. ~100 bytes pre-compression)

It’s not just bytes. Execution time also matters. The execution time for feature detection and running appropriate code (about 1ms on my MacBook Pro), is highly magnified on lower end devices like cheap Android phones selling for $50 or $100 in emerging countries.

Having hints would save bytes + execution time, and as more apps are server-side rendered for performance gains, more feature detection will have to be indicated to servers.

I’m measuring 0.2ms on my 2016 MBP, but that still seems like a lot. Most of it is coming from the fact that the feature detection snippet calls createElement() and it’s being slower than (IMO) it should be.

IMO, the right way to fix this would be to optimize that call away.

I measured it (similar code, but needs to do more like grab nonce: document.querySelector('script[nonce]')) which was 0.81ms on my MacBook. Also, the additional JS is much more than my example here (need to handle errors, fallback if events don’f fire, etc).