Update a Single Page App on Code Change Without Draining The Battery
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.