Resource-Adaptive Vue Apps

19 July, 2019

The web platform is ever changing, browsers offer us amazing capabilities that we never dreamed to have. You can tell if the user is offline and adapt to the situation, but you can do a whole lot more.

Depletable Resources

First you need know what's available and then we see how to use it, for that task, take a long look at the MDN's list of web APIs.

There is a couple of APIs that I will focus on in this article, the rest is up to you and your imagination.

Out of the available APIs, two stand out: The Battery Status API and the Network status API. Although both are experimental and not widely supported, let's try to see how can we use them to our advantage.

The two APIs can report the status of a depletable resource like the device battery and the network connection. By depletable I mean that they can run out or can influence our app behavior considerably.

The Battery Status API

The MDN sums up this API perfectly:

The Battery Status API, more often referred to as the Battery API, provides information about the system's battery charge level and lets you be notified by events that are sent when the battery level or charging status change. This can be used to adjust your app's resource usage to reduce battery drain when the battery is low, or to save changes before the battery runs out in order to prevent data loss.

That means we can use this API to change certain behavior in our app according to the battery level, for example if your app has a feed like twitter, you can turn off auto-playing videos if their charge level is low or if they are not currently charging.

The APIs is fairly easy to work with, but how could we integrate this in our Vue apps?

If you know me and if read my previous articles, you will know I'm a huge fan of scoped slots.

So we will expose the Battery status using slot-props, great! Now we need to actually code it.

A couple of observations we can make is that the Battery API is a global API, meaning the same values, listeners, events are the same for all the components. So it would be ideal if we implement our component to have a singleton reactive state instead of per-instance state.

We can do this with stateful functional components, let's start by building the state object:

import Vue from 'vue';

const state = Vue.observable({
  isCharging: false,
  timeToCharge: 0, // Time until the battery is full, if the system is charging.
  timeToEmpty: 0, // Time until the battery is depleted
  level: 1 // 1.0 is full
});

After we decided our state, we can start building our component.

// Reference to the battery manager.
let batteryManager = null;

export const Battery = {
  functional: true,
  render(_, { scopedSlots }) {
    const children = scopedSlots.default(state);

    // SSR Handling.
    if (typeof window === 'undefined') {
      return children;
    }


    // Graceful handling if the API isn't available.
    if ('getBattery' in navigator) {
      navigator.getBattery().then(b => {
        // Update the reference
        batteryManager = b;
      });
    }

    return children;
  }
});

So far so good, we still need to watch for the changes published by the API events. So we will add listeners as soon as we got a reference to our BatteryManager instance.

function addListeners() {
  if (!batteryManager) return;

  batteryManager.onchargingchange = () => {
    state.isCharging = batteryManager.charging;
  };
  batteryManager.onchargingtimechange = () => {
    state.timeToCharge = batteryManager.chargingTime;
  };
  batteryManager.ondischargingtimechange = () => {
    state.timeToEmpty = batteryManager.dischargingTime;
  };
  batteryManager.onlevelchange = () => {
    state.level = batteryManager.level;
  };
}
if ('getBattery' in navigator) {
  navigator.getBattery().then(b => {
    // Update the reference
    batteryManager = b;
    addListeners();
  });
}

The last missing piece for our test is that we still need to read the initial information and update the state:

function updateBatteryInfo() {
  if (!battery) return;

  state.isCharging = battery.charging;
  state.timeToCharge = battery.chargingTime || 0;
  state.timeToEmpty = battery.dischargingTime || 0;
  state.level = battery.level;
}

// In our render fn
if ('getBattery' in navigator) {
  navigator.getBattery().then(b => {
    // Update the reference
    batteryManager = b;
    updateBatteryInfo();
    addListeners();
  });
}

Hold it 🤚, did you catch that? While our sample would work fine we still have a problematic issue. If you check the render function you will notice that every time we render this component, we fetch a new manager and add a bunch of listeners which is not very ideal.

We can fix that by adding a simple boolean isDoneSetup.

let isSetupDone = false;

And in our render function:

if (!isSetupDone && 'getBattery' in navigator) {
  (navigator as any).getBattery().then((b: any) => {
    battery = b;
    updateBatteryInfo();
    addListeners();
    isSetupDone = true;
  });
}

You can see that in action here, and of course you need to use it on a Chrome browser with a device that has battery like a laptop or a phone.

Sadly this API is marked as obsolete and firefox while used to support it, removed it due to privacy concerns. Still it seems going strong in Chrome so far.

This API allows to build apps that adapt to the user battery level and accordingly help them use our app without eating away their resources.

Here another idea, aside from dark and light themes. You could add a performance theme where the background color is #000 while avoiding very bright colors.

The Connection API

This is another great API, although marked experimental, it could give us new ways to adapt to our users that have bad internet connection. After all large area of the world is still rocking the 3G connections, even less in some areas.

Let's create a component that allows us to:

  • Tell if the user is online or not, and when did they go offline.
  • Their current estimated downlink speed and how fast is their connection classified (4g, 3g).
  • If they are using the data saver mode.
  • Their network type (wifi, cellular, etc...).

Again let's begin by building the state:

import Vue from 'vue';

// we are prepared this time!
let isSetupDone = false;

const state = Vue.observable({
  isOnline: false,
  offlineAt: null,
  downlink: undefined,
  downlinkMax: undefined,
  effectiveType: undefined,
  saveData: undefined,
  type: undefined
});

And a function to add our listeners:

let onOnline;
let onOffline;

function addListeners() {
  onOffline = () => {
    state.isOnline = false;
    state.offlineAt = new Date();
  };

  onOnline = () => {
    state.isOnline = true;
    state.offlineAt = null;
  };

  window.addEventListener('offline', onOffline);
  window.addEventListener('online', onOnline);
  if ('connection' in window.navigator) {
    window.navigator.connection.onchange = onChange;
  }
}

And another function to set our initial state:

function updateConnectionProperties() {
  state.isOnline = window.navigator.onLine;
  state.offlineAt = state.isOnline ? null : new Date();
  // skip for non supported browsers.
  if (!('connection' in window.navigator)) {
    return;
  }

  state.downlink = window.navigator.connection.downlink;
  state.downlinkMax = window.navigator.connection.downlinkMax;
  state.effectiveType = window.navigator.connection.effectiveType;
  state.saveData = window.navigator.connection.saveData;
  state.type = window.navigator.connection.type;
  isSetupDone = true;
}

const onChange = () => updateConnectionProperties();

Putting all of this together in our functional component:

export default {
  functional: true,
  render(_, { scopedSlots }) {
    const children = scopedSlots.default(state);
    // SSR Handling.
    if (typeof window === 'undefined') {
      return children;
    }

    if (!isSetupDone) {
      updateConnectionProperties();
      addListeners();
    }

    return children;
  }
};

Here it is in action, this time I showcase how to load a photo with smaller resolution if the connection is slow:

There could be a small issues with its accuracy, but downlink prop seems to give us a good estimate on the user speed. You can try going offline, changing the network type, etc.

Not so surprisingly, this API is more accurate on Chrome for Android.

Conclusion

When it comes to those APIs, the sky is the limit on what you can do to make your users experience better. While browsers try to be efficient by not loading images, not playing videos, you could help them by making your app more aware of your users resources and adjust.