Skip to content
This repository has been archived by the owner on Jul 13, 2020. It is now read-only.

Latest commit

 

History

History
120 lines (88 loc) · 5.83 KB

README.md

File metadata and controls

120 lines (88 loc) · 5.83 KB

Battery-Friendly Timer

publication Archived Repository
The code of this repository was written to illustrate the blog post Update a Single Page App on Code Change Without Draining The Battery
This code is not intended to be used in production, and is not maintained.

Mobile applications using setInterval to poll a server are a battery hogs. Save battery life by fetching data at the right moment.

Motivation

Using AJAX polling on mobile is a very bad idea, because it drains the battery extremely fast. But why is that exactly?

Mobile devices use radio transmitters to communicate via 3G/4G, and their radio components are power hungry. Network providers and phone manufacturers have invented an escalation protocol to save battery life, called Radio Resource Control (RRC). Most of the time, a mobile device is idle, and uses low-power mode. When the user asks for an HTTP resource, the mobile device switches to high-power mode, downloads the resource, then switches back to low-power mode after a few seconds. The process of escalating to high power mode implies asking the radio tower for a dedicated channel and bandwidth allocation, and takes time and energy.

Therefore, mobile data network are optimized for burst: it's better to download all the data you need within a few seconds, then return to low-power mode. AJAX polling prevents the return to the low power mode, and keeps the device in high power mode until the battery is drained - even if it's only to download a few dozen bytes every minute or so.

Instead of polling a server at regular interval, you'd better call it when a download is already occurring. In that case, you know the device is in high power mode, and it's the ideal time to use the network.

The Battery-Friendly Timer listens to AJAX calls, and then triggers timeouts and intervals.

Usage

Usage is similar to setInterval, except you need to pass two delays instead of just one:

import timer from 'battery-friendly-timer';

timer.setInterval(
    () => fetch('http://my.api.url/').then(/* ... */),
    60 * 1000, // tryDelay: one minute
    60 * 60 * 1000 // forceDelay: one hour
);

setInterval takes two delays:

  • the first is the tryDelay. The timer does it best to trigger the callback at this interval, but only when the network is actively used. Therefore, if there is no network activity, the interval may never be called.
  • the second is the forceDelay. Whether there is network acticity or not, the callback will be triggered at that interval.

In the previous example, the server is polled every 60 seconds if there is an active HTTP connexion. If not, the server is polled every hour.

The Timer object provides setTimeout, clearTimeout, setInterval, and clearInterval methods. Apart from the forceDelay argument, the signature of these methods is the same as the window methods.

Example: Suggest refresh after code update of a Single-Page-Application

This scripts displays a banner inviting the user to reload the application if the JS code has changed on the server side:

<div id="update-available" style="position: absolute; top: 10px; right: 10px; padding: 1em; background-color: bisque; border-radius: 5px; display: none;">
    Myapp has a new version.
    <a href="#" onClick="window.location.reload(true);return false;">Click to reload</a>
</div>
import timer from 'battery-friendly-timer';

let previousHtml;
timer.setInterval(
    () => 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 */ }),
    5 * 60 * 1000, // tryDelay: 5 minutes
    24 * 60 * 60 * 1000 // forceDelay: 1 day
);

This works if you use Webpack, because the index.html is small, and includes a different cache buster param for the JS script each time you deploy. For instance, here is a typical index.html generated by Webpack:

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

In that case, comparing the HTML source is a fast and easy way to detect changes in the JS code.

If the HTML is bigger, instead of comparing the HTML source, you can compare a hash of the source. See for instance http://stackoverflow.com/a/7616484/1333479 for a fast and simple JS hash function for strings.

FAQ

Why not use Service Workers to intercept fetch()?

Because Service Workers are restricted to HTTPS, and require a more complex setup (loading an additional script).

Why not use push notifications?

If you can use push notifications (e.g. in a hybrid app), by all means do so. It's the best compromise in terms of reactivity and battery life. But web apps don't have an easy access to push notifications, and AJAX polling is the usual fallback.

Does it work if I use XHR instead of fetch()?

No, the timer only listens to fetch() calls. If you're still using XHR in a single-page application, it's time to make the switch.