Preloading network dependencies on hover<!-- --> | <!-- -->Web Performance Tips

Preloading network dependencies on hover

The browser's 'mouseover' event can be used as an explicit signal from your users indicating what their next action might be.

In this tip, we'll discuss how we can use the 'mouseover' event to preemptively preload assets and data to improve user perceived performance.

Prerequisites

When to use

In general, you can use this technique anytime you identify a key user scenario that's behind a user interaction, like a click or navigation, that depends on remote resources.

There are a variety of scenarios that fall into this category; the technique can be generalized.

Scenario: JavaScript SPA Apps

Modern web applications often have multiple SPA routes, modals, and interactive scenarios that load inline, and require additional acquisition of dependencies like JSON data and resources (JavaScript, CSS) to drive them. To enact these scenarios, users will click buttons within the web app.

Consider the following diagram:

A diagram of acquiring resources on click to drive a SPA UI

Note that the dependencies are not requested until after clicking the button.

Scenario: Static HTML Sites

Static HTML-based websites (ex: Wikipedia) that use full HTML page loads via Browser navigation rely on the user clicking <a> tags to navigate to the next page.

A diagram of acquiring HTML on navigation to drive a website

Note that the target page is not requested until after clicking the link.

In both the SPA and Static HTML scenarios, we can apply 'mouseover' based preloading to start fetching these remote dependencies earlier.

An example scenario

Let's consider a simple JavaScript SPA example.

In this example, the user will click a button to open a dialog that's subsequently presented on-screen:

const button = document.getElementById('button');

button.addEventListener('click', () => {
    // Acquire a remote dependency on click.
    fetch('data.json').then(res => res.json()).then(data => {
        // Use the acquired data to present the Dialog UI.
        renderDialog(data);
    });
});

For desktop users, the user will use their mouse to click the button:

A screenshot of a mouse hovering over an button

If we visualize how this is executed on the browser's main thread, it would look something like this:

A visualization of the JavaScript tasks for rendering this scenario on the Main Thread, including Network Time

In this visualization, a few things are clear:

  • The rendering of the dialog is dependent on the acquisition of data.json
  • Since data.json is a remote resource acquired over the network, the time to render our dialog is primarily dependent on:
    • The speed and reliability of the connection to the server data.json is hosted on.
    • The server time required to start transferring data.json to the browser
    • The transfer size of data.json
  • The Main Thread is idle while this remote data is being retrieved, further indicating the user scenario is network-bound.

Applying hover preloading

There is a finite amount of time between user 'mouseover' and 'click', and we can utilize this time to start preloading dependencies for whatever lies beyond that click to mask the network cost.

Let's adjust our sample code to apply the technique:

const button = document.getElementById('button');

let dataPromise;

// Fetches 'data.json' and stores that promise for subsequent use
// on 'dataPromise'
function fetchData() {
    if (!dataPromise) {
        dataPromise = fetch('data.json').then(res => res.json());
    }

    return dataPromise;
}

// Starting fetching the dependency on 'mouseover'
button.addEventListener('mouseover', () => {
    fetchData();
});

button.addEventListener('click', () => {
    // Uses pre-fetched 'dataPromise' if available
    fetchData().then(data => {
        // Use the acquired data to present the Dialog UI.
        renderDialog(data);
    });
});

Our code now will start fetching data.json when the user hovers over the button. When the user decides to click the button, the request will be in-flight or completed, speeding up the perceived time to render the dialog.

If we visualized this on our thread, it would look like this:

A diagram showing how the above example appears on the thread, with a reduced user perceived latency.

Other Resource Types

This technique is not limited to JSON data.

Static HTML navigation

For static web applications, we would use prefetch link tags:

// Get all `<a>` elements
const anchorTagElements = document.getElementsByTagName('a');

[ ...anchorTagElements ].forEach(anchor => {
    anchor.addEventListener('mouseover', event => {
        // Get the target href from the element.
        // i.e. "/tip/hover-preloading" from <a href="/tip/hover-preloading">
        const href = event.target.href;

        // Create a new <link rel="prefetch" href="/tip/hover-preloading"> element
        const link = document.createElement('link');
        link.rel = 'prefetch';
        link.href = href;
        document.head.appendChild(link);
    });
});

Code-Split Bundles

For JavaScript SPAs, we could use import(...) to preemptively load code-split modules:

button.addEventListener('mouseover', () => {
    // Preemptively import Dialog chunk
    import('./DialogComponent');
});

Mobile Users

For mobile users, use touchstart instead of mouseover as a reliable signal, since mobile users will not have mouse pointer events:

// Handle desktop scenario
button.addEventListener('mouseover', () => {
    preloadData();
});

// Handle mobile scenario
button.addEventListener('touchstart', () => {
    preloadData();
});

Additional Considerations

  • You may want to apply a "hover time threshold" if you find you are preloading dependencies too aggressively
  • Make sure you are not incurring too much backend load through these preload requests. You want to improve perceived performance, but not at too high of a cost on your servers.

Examining in the Profiler

I've put together an example application to demonstrate the technique.

The UI has two buttons, one without hover preloading and one with hover preloading. Each button retrieves a remote resource, and uses that to render the subsequent dialog.

For the button without the optimization applied, our time to render looks like this:

A screenshot of the Chromium F12 Profiler with no hover preloading

Notice that after clicking, the user cannot see anything until that network dependency is resolved.

For the button with the optimization applied, your results may vary depending on how long you hovered, and how fast your network is.

If we hover long enough over the button to fully preload the resource, the user perceives no network waiting time. In that case, the profiler result will look like this:

A screenshot of the Chromium F12 Profiler with the hover preloaded resource fully resolved

If you hover briefly and click quickly, you may see a result like this, indicating hover preloading didn't fully preload the resource, but it did give the resource loading a nice head start:

A screenshot of the Chromium F12 Profiler with the hover preloaded resource partially resolved

Conclusion

Preloading on hover can improve user perceived performance by preemptively acquiring these remote resources right before they are needed.

Make sure to measure the impact of hover preloading in your metrics to observe how much time your are improving your key user scenarios!

That's all for this tip! Thanks for reading! Discover more similar tips matching JS Optimization and Network.