The marmelab blog

Update a Single Page App on Code Change Without Draining The Battery

Published on 29 August 2016 by François Zaninotto with tags javascript ReactJS TDD

Single-Page Applications (SPAs) can run for a long time ; this makes deploying code updates harder. Users usually don’t hit the browser “Refresh” button to get the latest updates, unless notified to.

So each time you deploy a new version of the JS code of a SPA, you should notify users and invite them to refresh. If that seems easy to achieve, the simplest solution (AJAX polling) is also the worst, at least on Mobile. Read on to see the best way to remotely update a SPA without draining the battery.

Cheap Change Detection Using Webpack Hash

When building Web Apps for mobile, how can users know that the application code has changed? In an ideal world, you could notify each connected user about the change using push notifications. But push notifications on the web are still an early implementation, and require a complicated setup. We’ll explore another way.

The JS code on the client side could check if the SPA code is the same as the one it’s running. If there is a difference, it could display a banner inviting to reload.

But the JS code of a SPA is often quite heavy. It’s a waste of resource to force clients to download several hundred kB, just to make sure it’s the same version as the one previously downloaded. But if you use Webpack and HtmlWebpackPlugin, there might be an easier way. Take a look at this index.html for a webpack-compiled SPA with the hash=true configuration option:

<!DOCTYPE html>
<html>
    <head>
        <title>MyApp</title>
    </head>
    <body>
        <div id="root"></div>
        <script type="text/javascript" src="/main.js?184c0441d4c89b34ba08"></script>
    </body>
</html>

HtmlWebpackPlugin adds a cache buster parameter to the src attribute of the <script> tag. Each time the code changes, the cache buster changes, too. So instead of downloading the entire main.js, you just have to download the small index.html and compare it with the current version.

Something like:

<div id="update-available" style="display: none; position: absolute; top: 10px; right: 10px; padding: 1em; background-color: bisque; border-radius: 5px;">
    Myapp has a new version.
    <a href="#" onClick="window.location.reload(true);return false;">Click to reload</a>
</div>
let previousHtml;
function checkUpdatesInCode() {
    return fetch('http://my.app.url/index.html')
        .then(response => {
            if (response.status !== 200) {
                throw new Error('offline');
            }
            return response.text();
        })
        .then(html => {
            if (!previousHtml) {
                previousHtml = html;
                return;
            }
            if (previousHtml !== html) {
                previousHtml = html;
                document.getElementById('update-available').style.display = 'block';
            }
        })
        .catch(err => { /* do nothing */ });
}

checkUpdatesInCode();
// and then again in the future
checkUpdatesInCode();

If the HTML is heavy, or if you want to compare a hash instead of a string, use your favorite string hash function (for instance, a fast and concise JS implementation of Java’s hashCode).

setInterval Used For Periodic Fetch Considered Harmful

In order to regularly check updates, you could be tempted to use setInterval:

checkUpdatesInCode();
setInterval(checkUpdatesInCode, 10 * 60 * 1000); // check every 10 minutes

This is a very bad idea on mobile networks. And to understand why, you need to understand how 3G/4G networks work.

The radio component of a mobile device can switch between several power states, in what is called the Radio Resource Control (RRC) protocol. Most of the time, a mobile device is idle, and uses low-power radio mode. When the user asks for an HTTP resource, the mobile device switches to the “connected” mode, in which the radio component consumes a lot of power. The device downloads the resource, then switches back to low-power mode after a few seconds. The process of escalating to connected mode implies asking the radio tower for a dedicated channel and bandwidth allocation, and takes a fixed power cost.

Ilya Gregorik explains it very clearly in his book “High Performance Browser Networking” (a must read for web developers):

There is no such thing as a “small request” as far as the battery is concerned.

Intermittent network access is a performance anti-pattern on mobile networks

Polling is exceptionally expensive on mobile networks; minimize it

He estimates that a single application polling every minute consumes about 600 joules of energy per hour, or 3% of total battery capacity. Open a few applications like that in the background and your battery is empty at noon.

Aggregate Outbound and Inbound Requests

The solution is simple: check for updates when the radio is already in connected mode. The cost of an additional HTTP request in that case is marginal.

A good way to know when a webapp is connected is to decorate the fetch() function:

const originalFetch = window.fetch;
window.fetch = function () {
    notifyNetworkIsAvailable();
    return originalFetch.apply(win, arguments);
};

Then the notifyNetworkIsAvailable function can do the polling, if the previous polling is old enough. setInterval could still be used to force an update even if there was no network connectivity for a long time (e.g. every day).

In fact, this logic could packaged into a special timer object, exposing a familiar API inspired by setInterval():

timer.setInterval(
    checkUpdatesInCode,
    10 * 60 * 1000, // check every 10 minutes if network is available
    24 * 60 * 60 * 1000 // check every day even if no network activity
);

This battery-friendly timer didn’t exist, so we wrote it, and released it under an open-source license. It’s called battery-friendly-timer, and you can use it as follows:

import timer from 'battery-friendly-timer';

checkUpdatesInCode();
timer.setInterval(
    checkUpdatesInCode,
    10 * 60 * 1000, // check every 10 minutes if network is available
    24 * 60 * 60 * 1000 // check every day even if no network activity
);

Bonus: The React <AutoReload> Component

If you’re not fond of getElementById() and prefer the React.js approach, it’s fairly easy to create a component building up on the battery-friendly-timer logic to display a banner when it detects a new version of the code:

import React, { Component, PropTypes } from 'react';
import timer from 'battery-friendly-timer';

class AutoReload extends Component {
    constructor(props) {
        super(props);
        this.previousHash = null;
        this.state = {
            codeHasChanged: false,
        };
        this.fetchSource = this.fetchSource.bind(this);
    }

    componentDidMount() {
        const { tryDelay, forceDelay } = this.props;
        this.fetchSource();
        this.interval = timer.setInterval(this.fetchSource, tryDelay, forceDelay);
    }

    componentWillUnmount() {
        timer.clearInterval(this.interval);
    }

    fetchSource() {
        return fetch(this.props.url)
            .then(response => {
                if (response.status !== 200) {
                    throw new Error('offline');
                }
                return response.text();
            })
            .then(html => {
                const hash = this.hash(html);
                if (!this.previousHash) {
                    this.previousHash = hash;
                    return;
                }
                if (this.previousHash !== hash) {
                    this.previousHash = hash;
                    this.setState({ codeHasChanged: true });
                }
            })
            .catch(() => { /* do nothing */ });
    }

    /**
     * Java-like hashCode function for strings
     *
     * taken from http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery/7616484#7616484
     */
    hash(str) {
        const len = str.length;
        let hash = 0;
        if (len === 0) return hash;
        let i;
        for (i = 0; i < len; i++) {
            hash = ((hash << 5) - hash) + str.charCodeAt(i);
            hash |= 0; // Convert to 32bit integer
        }
        return hash;
    }

    reloadApp(e) {
        window.location.reload(true);
        e.preventDefault();
    }

    render() {
        if (!this.state.codeHasChanged) return null;
        const style = {
            position: 'absolute',
            top: 10,
            right: 10,
            padding: '1em',
            zIndex: 1050,
            backgroundColor: 'bisque',
            borderRadius: 5,
            textAlign: 'center',
        };
        return (
            <div style={style}>
                <div>Myapp has a new version.</div>
                <div><a href="#" onClick={this.reloadApp}>Click to reload</a></div>
            </div>
        );
    }
}

AutoReload.propTypes = {
    url: PropTypes.string.isRequired,
    tryDelay: PropTypes.number.isRequired,
    forceDelay: PropTypes.number.isRequired,
};

AutoReload.defaultProps = {
    url: '/',
    tryDelay: 5 * 60 * 1000, // 5 minutes
    forceDelay: 24 * 60 * 60 * 1000, // 1 day
};

export default AutoReload;

And there you have it, an auto reload component that you can include in the main layout of a mobile web app:

<AutoReload url="/index.html" tryDelay={10 * 60 * 1000} />

Conclusion

Native apps don’t have to deal with code updates - the Android/iOS stores take care of if for them. But WebApps must take care of it themselves, and it’s not as simple as it seems.

Remember to be gentle with your mobile users’ battery. Even if many people walk in the street with a portable backup battery these days (Pokemon Go is a battery hog), it’s not a reason to consume the entire battery capacity in hours.

comments powered by Disqus