Skip to content

Meteor Web Apps on Mobile

Collin Donahue-Oponski edited this page Mar 20, 2014 · 7 revisions

This page summarizes issues I know about related to writing web applications on mobile devices using Meteor.

See also Improve mobile browser experience on the Meteor Roadmap.

The focus here is only on web applications. Some of these issues here don't matter for native apps running in PhoneGap and the like, and native apps likely have other needs not covered here. (See for example https://github.com/awatson1978/cordova-phonegap)

Issues resolved

  • Touch events are now supported.

  • Excessive reconnect timeout has been reduced.

  • Meteor now automatically reconnects when the browser reports that it has transitioned to being online.

  • the Internet activity spinner icon no longer appears continuously in iOS when websockets is supported (iOS >= 6).

Unwanted highlighting on Android

See Meteor issue #734: Android artifacts.

We expect this will be fixed in the upcoming Meteor UI, as it will be using jQuery for events.

When a "click" handler is attached to any element on the page, all elements on the page become clickable after they've been tapped on once. The Android browser highlights "clickable" elements when they are tapped.

To capture event bubbling and provide real events to the application, Meteor's universal-events package temporarily attaches a second event handler to the target of the event. (events-w3c.js:117) Attaching a click event handler in this way triggers the Android "clickable" behavior.

A workaround is to not attach a click handler anywhere on the page (you can use the tap event instead).

A possible alternative is to take jQuery's approach of synthesizing bubble events. The disadvantage is that we're no longer delivering actual DOM Event objects to the application, and it takes extra work to be sure that we're exactly matching the bubbling behavior of the browser.

jquery-events is a proof-of-concept implementation of universal-events demonstrating that not attaching a second event handler fixes the issue (it uses jQuery internally). It hasn't been tested for compatibility with Meteor, but it does seem to be able to run the "todos" example.

Battery life and data charges

Mobile devices range from tablets used to stream movies over Wifi (where the additional overhead of a Meteor data connection is negligible) to phones connecting over 3G or even 2G (where having the network connection powered up puts a noticeable load on the battery, and data charges may be expensive).

A thought is to have a "low power" mode which for example polls occasionally instead of continuously. On Android it's possible to detect whether the device is connecting over 3G using navigator.connection.type. Perhaps for devices where we can't detect the connection type there could be a UI to allow the user to choose to put the app into low power mode. We could also go into "low power" mode in inactive tabs (though on iOS this happens today anyway because of the suppression of timeout events).

For myself, the kinds of applications where I care most about Meteor's real-time capabilities and sophisticated UI tend to be more on the "tablet and WiFi" side of the spectrum than on phones with limited bandwidth. Thus I'd prefer to see "full power" mode be the default (so that Meteor works just as well on tablets as it does on the desktop), with "low power" mode available for when we can detect that it should be used or when requested by the user.

(As a side note, native iOS apps are able to detect 3G, so Meteor apps using PhoneGap would be able to put themselves into "low power" mode).

Remote debugging

zol reports that "remote debugging via Chrome and Safari is possible and actually works great: see Safari-Remote-Debugging"

setTimeout in inactive iOS tabs

iOS holds off delivering setTimeout and setInterval events in inactive tabs until the tab becomes active again.

Some blog posts say that inactive tabs are "suspended", but this isn't true. The tab will still respond to AJAX and storage events. Thus for example, an inactive tab can make a long poll request to a server, handle the return result, and make another long poll request... and continue indefinitely as long as the server does. (But if it loses the connection it has no way to set a timeout and try again).

Meteor uses setTimeout(fn, 0) to run things in the next tick of the event loop in Spark and in Deps, and in an inactive tab the call to fn won't happen until the tab becomes active again.

I haven't seen an example yet where this causes a problem for users (it just delays reactive updates until the user switches back to the tab, as far as I know), but it does make testing cross-tab code a pain when Meteor's reactive system shuts down in inactive tabs.

PR #1023 implements Meteor.defer with the best available implementation for running code in the next pass of the event loop (which on iOS along with many browsers turns out to be using postMessage to send a message to the current window).

This decouples "run code in the next tick of the event loop" (which now always works) from "run code after a timeout" (which remains throttled in iOS for inactive tabs).

Note that it may be desirable to deliberately defer running some code in inactive tabs. (For example, we might not need to update the UI continuously when no one can see it). There's a couple of ways to do this. In iOS it's easy for code to detect whether its tab has become inactive (simply start a recurring timer with setInterval, and if the last time your callback was called is longer than the timer period you know you've been throttled). And if the behavior you want is "run some code in the next pass of the event loop, but not while the tab is inactive", setTimeout(fn, 0) will continue to do that.

Losing user changes when switching between tabs

When a mobile device is low on memory, it will unload a tab when the user switches to another tab or application that needs more memory than is available.

An unloaded tab retains its position in the tab list and its URL, but its memory and assets such as its JavaScript code and HTML and CSS are cleared. When the user returns to the tab, the tab is loaded again fresh (as if the user had just navigated to the URL, or had reloaded the page).

In HTML5, session storage is designed to be a place to store data needed by a particular tab. Sadly on iOS session storage is also cleared when a tab is unloaded (which makes session storage rather useless on iOS).

In Meteor, updates made by the user are stored in JavaScript memory until they can be sent up to the server. If the device is temporarily offline and the user switches to a different tab or application, changes the user has made will be lost if the tab is unloaded.

Offline data is a large project that includes as one of its goals saving user changes persistently in the browser until they can be sent up to the server.

There could also be a less comprehensive solution for just this particular issue of losing user changes when switching tabs (without also, for example, saving user changes even if a tab is closed) by persisting user changes associated with the tab. This is complicated by session storage not being useful on iOS and the lack of tab identity, though it would appear to be possible by emulating session storage.

Meteor's Session

The data in Meteor's Session is associated with a particular tab (each browser tab has its own separate session data), and is preserved across hot code reloads but not page refreshes. It thus suffers the same problem as losing user changes: you can lose your session state merely by switching between tabs and applications if the device happens to unload the tab on you.

Session is an old part of the Meteor API (it was created before the Deps system was published), and so originally was used for whatever state didn't have its own representation (such as collections and login status)... regardless of whether that state was really tied to the particular browser tab or not.

For example, in the "todos" example application the URL is updated with the id of the selected list (and so if you bookmark the app while looking at a particular list and come back to the bookmark, you'll see the same list selected again). Since this particular bit of state is associated with the URL, it doesn't also need to be associated with the browser tab, and an implementation written today could use Deps to associated the currently selected list id with the URL instead of putting it into Session.

A question then becomes, is there any application state that we actually need or want to be associated with a particular browser tab?

  • A FAQ is how to save drafts in the browser so that they aren't pushed to the server until the user finishes them; but an alternative is to save the draft to the server but mark it as "unpublished" and invisible to other users until the user "saves" or "publishes" it. This allows a logged-in user to switch to a different device and not lose their draft (similar to how drafts are saved in gmail, for example).

  • Like the "todos" example, more or less of the UI state may be usefully associated with the URL, which allows the user to bookmark or share a particular document or view within the application, instead of only the application itself.

If there is some state that we'd like to be preserved when A) the user returns to a tab, even if the tab was unloaded by the device, and B) that state is specific to the tab and not associated with all tabs in the browser or with the logged in user, but C) we'd rather not see that state be carried along with a shared or bookmarked URL... then we'll want a session storage implementation.

Emulating Session Storage

As mentioned above, in HTML5 session storage is designed to be a place to store state associated with a particular tab... which unfortunately is useless on iOS because it is cleared along with everything else when a tab is unloaded.

If there was some way to identify a tab, that is if each tab had a unique identifier, or if we could give each tab a unique identifier, then that identifier could be used as a key into storage which is not cleared when a tab is unloaded (such as local storage or a browser database). Naturally we'd need some mechanism for clearing out old data, but that could be a simple rule such as "clear out data for tab identifiers which haven't been used in the past X days".

But, I wasn't able to find anything that was preserved across tab unloads in iOS, aside from the URL.

Simply generating a random string to append to the URL with a hash mark identifier isn't sufficient because the user could bookmark or otherwise copy or share the URL, and we'd end up with multiple tabs with the same identifier.

And, if a tab is opened or reloaded with a particular identifier we don't know if any other tabs might or might not be already using that identifier. (We couldn't, for example, use cross-tab messaging to ask if any tabs are already using the identifier, because an unloaded tab wouldn't reply).

However, whenever a tab was opened or reloaded with an identifier we could immediately and unconditionally clone the data associated with the identifier and then use a new identifier. (This would generate more copies of saved tab state, but we'd need to clean out old tab state anyway). This preserves state across tab unloads, while giving different tabs separate state.

While there's no implementation for this yet and so I may have missed something, as far as I can tell just thinking about it I think this would work as a mechanism to associate state with particular tabs on iOS, even across tab unloads.

Offline Support

While somewhat orthogonal to mobile (desktop web apps can be offline, and mobile apps can be online-only), people do tend to want to especially use mobile apps offline.

Offline application cache

The appcache project implemented an offline application cache for Meteor, which allows the static parts of an application (the HTML, Javascript, CSS, and images) to be cached offline.

The app cache isn't too useful by itself (well, it does have some secondary advantages such as lessening the page refresh time on hot code reloads :), but it is a necessary component of supporting offline use.

Even if you didn't care able being able to launch an application offline, and just wanted your Meteor app to be able to survive losing the Internet connection occasionally, the app cache would still be important. The tab unloading behavior means that without the app cache the browser needs to be able to connect to the server to reload the app if it's been unloaded.

Offline data

The Offline Data Project is implementing an "Offline Collection" which can be used in place of a standard Meteor.Collection:

  • Documents from server collections are stored persistently in the browser database, making them available to the application even if the application starts up offline.

  • Changes made by the user are also saved in the browser database, preserving them if tabs are unloaded by the device, tabs are closed, or the browser is closed and reopened. The next time the application is opened and the browser goes online the user's changes are sent up to the server.

  • Updates are reactively shared across browser tabs open on the same application even while offline.

A trade-off of using this full offline data implementation is that there are some API changes needed compared to the Meteor.Collection API, and so it's not a transparent extension to Meteor.

Someone who didn't need all the features of the offline data project might prefer a more incremental, simpler, and/or transparent implementation. For example, if you cared about not losing user changes when switching between tabs, but not about preserving user updates if a tab is closed or about reactively sharing updates between tabs, you might be able to more simply store user updates in session storage (and emulating session storage if needed).