diff --git a/aleksis/core/assets/components/app/App.vue b/aleksis/core/assets/components/app/App.vue
index 797feda4dfbabc3f579849c452bb3aee623da5be..fddbb3378e79302b1b12fa1d7b92eb5c5951540a 100644
--- a/aleksis/core/assets/components/app/App.vue
+++ b/aleksis/core/assets/components/app/App.vue
@@ -349,9 +349,9 @@ import gqlWhoAmI from "./whoAmI.graphql";
 import gqlMessages from "./messages.graphql";
 import gqlSystemProperties from "./systemProperties.graphql";
 import gqlSnackbarItems from "./snackbarItems.graphql";
-import gqlPing from "./ping.graphql";
 
 import useRegisterSWMixin from "../../mixins/useRegisterSW";
+import offlineMixin from "../../mixins/offline";
 import menusMixin from "../../mixins/menus";
 
 export default {
@@ -362,34 +362,9 @@ export default {
       systemProperties: null,
       messages: null,
       snackbarItems: null,
-      ping: null,
     };
   },
-  methods: {
-    toggleGlobalQueries(enable, pingPolling) {
-        console.debug("Toggling global queries: " + (enable ? "on" : "off"));
-	if (enable) {
-	  this.$apollo.queries.whoAmI.startPolling(10000);
-          this.$apollo.queries.messages.startPolling(1000);
-  	} else {
-	  this.$apollo.queries.whoAmI.stopPolling();
-          this.$apollo.queries.messages.stopPolling();
-        }
-        if (pingPolling) {
-          console.debug("Starting ping polling.");
-          this.$apollo.queries.ping.observer.resetLastResults();
-          this.ping = null;
-	  this.$apollo.queries.ping.startPolling(1000);
-        } else {
-	  this.$apollo.queries.ping.stopPolling();
-	}
-    },
-
-  },
   apollo: {
-    ping: {
-      query: gqlPing,
-    },
     systemProperties: gqlSystemProperties,
     whoAmI: {
       query: gqlWhoAmI,
@@ -428,32 +403,6 @@ export default {
       },
       deep: true,
     },
-    ping: function (ping) {
-      if (ping === "pong") {
-        console.debug("Pong was received. Resuming regular queries.");
-        this.toggleGlobalQueries(true, false);
-        this.$root.offline = false;
-      }
-    },
-    "$root.offline": function (offline) {
-      if (navigator.onLine && !document.hidden) {
-        console.debug("Queries failed, but navigator is online.");
-        this.toggleGlobalQueries(false, true);
-      }
-    },
-  },
-  mounted() {
-    window.addEventListener("online", (e) => { console.debug("Navigator changed status to online."); if (!document.hidden) this.toggleGlobalQueries(false, true); });
-    window.addEventListener("offline", (e) => { console.debug("Navigator changed status to offline."); this.toggleGlobalQueries(false, false); });
-    document.addEventListener("visibilitychange", () => {
-      if (document.visibilityState === "hidden") {
-        console.debug("Visibility changed status to hidden.");
-        this.toggleGlobalQueries(false, false);
-      } else if (navigator.online) {
-        console.debug("Visibility changed status to visible. Navigator status is online.");
-        this.toggleGlobalQueries(false, true);
-      }
-    });
   },
   name: "App",
   components: {
@@ -466,7 +415,7 @@ export default {
     Loading,
     SnackbarItem,
   },
-  mixins: [useRegisterSWMixin, menusMixin],
+  mixins: [useRegisterSWMixin, offlineMixin, menusMixin],
 };
 </script>
 
diff --git a/aleksis/core/assets/mixins/offline.js b/aleksis/core/assets/mixins/offline.js
new file mode 100644
index 0000000000000000000000000000000000000000..40b10b3b41ba1a8d15585e37ef7e05789a534db2
--- /dev/null
+++ b/aleksis/core/assets/mixins/offline.js
@@ -0,0 +1,87 @@
+import gqlPing from "../components/app/ping.graphql";
+
+/**
+ * Mixin for handling of offline state / background queries.
+ * 
+ * This handles three scenarios:
+ *   - The navigator reports that it is in offline mode
+ *   - The global offline flag was set due to network errors from queries
+ *   - The navigator reports the page to be invisible
+ * 
+ * The main goal is to save bandwidth, energy and server load in error
+ * conditions, or when the page is not in focus. This is achieved by a
+ * fallback strategy, where all background queries are stopped in offline
+ * state, and only a ping query is sent once the navigator reports itself
+ * as online and the app gets into focus. Once this ping query is successful,
+ * background activity is resumed.
+ */
+const offlineMixin = {
+  data() {
+    return {
+      ping: null,
+    };
+  },
+  mounted() {
+    window.addEventListener("online", (e) => {
+      console.debug("Navigator changed status to online.");
+      if (!document.hidden) this.toggleGlobalQueries(false, true);
+    });
+    window.addEventListener("offline", (e) => {
+      console.debug("Navigator changed status to offline.");
+      this.toggleGlobalQueries(false, false);
+    });
+    document.addEventListener("visibilitychange", () => {
+      if (document.visibilityState === "hidden") {
+        console.debug("Visibility changed status to hidden.");
+        this.toggleGlobalQueries(false, false);
+      } else if (navigator.online) {
+        console.debug(
+          "Visibility changed status to visible. Navigator status is online."
+        );
+        this.toggleGlobalQueries(false, true);
+      }
+    });
+  },
+  methods: {
+    toggleGlobalQueries(enable, pingPolling) {
+      console.debug("Toggling global queries: " + (enable ? "on" : "off"));
+      if (enable) {
+        this.$apollo.queries.whoAmI.startPolling(10000);
+        this.$apollo.queries.messages.startPolling(1000);
+      } else {
+        this.$apollo.queries.whoAmI.stopPolling();
+        this.$apollo.queries.messages.stopPolling();
+      }
+      if (pingPolling) {
+        console.debug("Starting ping polling.");
+        this.$apollo.queries.ping.observer.resetLastResults();
+        this.ping = null;
+        this.$apollo.queries.ping.startPolling(1000);
+      } else {
+        this.$apollo.queries.ping.stopPolling();
+      }
+    },
+  },
+  apollo: {
+    ping: {
+      query: gqlPing,
+    },
+  },
+  watch: {
+    ping: function (ping) {
+      if (ping === "pong") {
+        console.debug("Pong was received. Resuming regular queries.");
+        this.toggleGlobalQueries(true, false);
+        this.$root.offline = false;
+      }
+    },
+    "$root.offline": function (offline) {
+      if (navigator.onLine && !document.hidden) {
+        console.debug("Queries failed, but navigator is online.");
+        this.toggleGlobalQueries(false, true);
+      }
+    },
+  },
+};
+
+export default offlineMixin;