Developing websites for modern mobile devices has a pitfall you may not be aware of: in-app browsers. These are web browsers embedded directly within native mobile apps. So if a link is clicked within a native app (e.g. Instagram or TikTok), it uses the in-app browser instead of switching apps to a dedicated browser app.
While potentially convenient for mobile developers (i.e. users will never leave our app! the businessmen squeal), we’ll discuss the drawbacks for web developers like yourself and your users.
In-app browsers are also referred to as embedded browsers or WebView. These are interchangeable terms.
The Drawbacks
The drawbacks of in-app browsers can be broadly categorized:
Limited functionality
In-app browsers are considerably stripped down when compared to their fully-featured counterparts and typically lack features like bookmarking, UI controls, settings, extensions, and downloads. For for instance a browser extension that a user depends on or help protect their privacy will not work in an in-app browser.
Privacy & security concerns
Because in-app browsers are embedded within a native mobile app, the app developer has control and visibility into the users’ in-app browsing activity. This even extends into being able to inject code into the in-app browser which is a major privacy and security concern. Users are largely unaware and aren’t able to opt-out even if they are.
Inconsistent UI/UX
Because in-app browser implementations are all different, the UI is inconsistent. Further, browsing data like history and bookmarks aren’t shared so users typically need to sign into services they may already be securely signed into in their devices actual browser. This leads to a fragmented and frustrating user experience.
Worse performance
In-app browsers tend to be running outdated browser internals which can cause slower loading times and compatibility issues. Users on slower Internet connections may have the problem exacerbated.
Author Update: Since Apple doesn’t allow apps, even browsers, to use their own rendering engine only Android has the problem of bundling in a custom in-app browser instead of using the system WebView, which may be outdated and have worse performance. On iOS, the built-in WebView is bundled as part of the iOS WebKit. On Android, the default built-in WebView is based on the Blink version and is updated independently of the OS as part of the Chrome update process via Google Play.
Bad Behavior in In-App Browsers
History
In-app browsers have existed since circa 2016, but it wasn’t until 2019 when Thomas Steiner, a Google engineer, published a blog post that dove into Facebook’s iOS and Android apps that a wider audience was made aware of the privacy and security concerns. Thomas discussed the technical details behind how the apps implemented their in-app browsers and stated how in-app browsers can perform man-in-the-middle (MITM) attacks by injecting arbitrary JavaScript code and intercepting network traffic.
Three years later, in 2022, Felix Krause published two blog posts (1, 2) and a tool, inappbrowser.com, which focused on the privacy concerns of iOS apps. Initially this covered apps by Meta (Facebook, Messenger, Instagram) and then followed up with Android and other social media apps including TikTok. Felix’s findings supported Thomas’ from 3 years earlier and showed concerning findings from iOS Instagram: the arbitrary injection of a pcm.js script which Meta claimed to be an “event aggregator” but was also monitoring user interactions in the form of taps and selections. Further cause for concern was TikTok injecting JavaScript that monitors all keyboard inputs along with taps, which is effectively the functionality of a keylogger on third-party sites. TikTok acknowledged the existence of this code but claimed it’s only used for debugging, troubleshooting, and performance monitoring.
Felix’s findings led to a lawsuit being filed against Meta in September 2022. The case was dismissed in October 2023.
Nothing Has Changed
Let’s revisit the behavior of iOS & Android Instagram’s in-app browser at the time of this writing (July 2024). This is done by sharing the two testing links, inappbrowser.com and inappdebugger.com (we’ll discuss this one more shortly), in the app as a direct message or URL in your profile bio. This is so you can actually click on them, as Instagram prevents clickable URLs in places like the descriptions of posts.
Let’s start with iOS. Below is iOS Instagram opening inappbrowser.com
and inappdebugger.com
in July 2024:
This shows that iOS Instagram is still injecting arbitrary JavaScript code which listens to user clicks along with JavaScript messages.
(Editor note: when testing this I noted that Instagram also appends URL parameters on outgoing links, which may be used to communicate additional information to this injected JavaScript).
Next, Android.
The story on Android is slightly different: there’s still arbitrary JavaScript being injected but it isn’t necessarily listening to events tightly coupled with user interactions.
Unfortunately, not much has changed since Felix’s findings nearly 3 years ago.
Open Web Advocacy wrote a piece earlier this year following the events of Apple threatening to kill web apps.
Debugging & Detecting In-App Browsers
Leveraging the existing excellent work of Felix Krause and Shalanah Dawson we have strategies for debugging and detecting when our websites are being viewed by in-app browsers.
- https://inappbrowser.com/
- Attempts to detect if there’s any injected JavaScript code running.
- https://inappdebugger.com/
- Attempts to detect you’re in an in-app browser and, if so, which app it is inside of.
- Additionally provides some debugging tests for if downloads are possible and escape-hatches for getting to an actual device browser.
- Leverages both inapp-spy and bowser.
- https://github.com/bowser-js/bowser
- A browser detection library providing metadata and filtering based on browser version.
- https://github.com/shalanah/inapp-spy
- A TypeScript library written by Shalanah Dawson that aids in detecting in-app browsers.
Escaping
Now that we have some tools, let’s look at a example in JavaScript for detecting and redirecting in Android using an intent:
link. You’d do this if you simply do not want your website being opened in an in-app browser, and offer a link to users to open it in their default browser instead.
import InAppSpy from "inapp-spy"
const { isInApp } = InAppSpy()
// Your app's full URL, maybe defined build-time for different environments
const url = `https://example.com`
const intentLink = `intent:${url}#Intent;end`
// 1. Detect in-app
if (isInApp) {
// 2. Attempt to auto-redirect
window.location.replace(intentLink)
// 3. Append a native <a> with the same intent link
const $div = document.createElement("div")
$div.innerHTML = `
<p>Tap the button to open in your default browser</p>
<a href="${intentLink}" target="_blank">Open</a>
`
document.body.appendChild($div)
}
Code language: JavaScript (javascript)
It’s not ideal to have to load extra JavaScript for this, but it is reliable. This may be heavy handed, but for those of you working on particularly sensitive sites, it might be worth doing.
To get an idea of a way this can be implemented, Shalanah’s inappdebugger.com provides this functionality under the “Android In-App Escape Links” section.
Unfortunately, there’s currently no reliable way of handling iOS in-app browsers in terms of a proper escape hatch. Similar to Android, there’s a handful of device-specific URI schemes (that’s technically what the intent:
prefix is called), but none of them are reliable to the default browser on a specific URL. A not-so-great workaround is using the x-web-search://?
scheme, but the best-case is using the site:
search prefix to get close to your actual URL e.g. x-web-search://?site:example.com
.
Author Update: a somewhat reliable iOS workaround has been documented and tested by trying to run a Shortcut that doesn’t exist, specifying your URL in an error callback, and opening that in the user’s default browser. In practice, this looks like:
shortcuts://x-callback-url/run-shortcut?name=${crypto.randomUUID()}&x-error=${encodeURIComponent('https://example.com')}
This comes with some side effect caveats: the Shortcuts app is opened on the user’s device and some query parameters are appended to your URL. Read more on GitHub.
A last-ditch effort on iOS would be creating a UI element in your web app that gives the user manual instructions for bailing:
- Tapping the “…” menu
- Tapping on “Open in browser”
This is considerably more fragile and error-prone, but if you have the metrics to where your user traffic is coming from and which in-app browser is preventing them from converting to your feature-rich PWA then it could be worth considering.
Hopefully, with time, we’ll see the fall of in-app browsers. The privacy and security concerns alone are unacceptable. Couple that with the limited functionality and poor user experience, it’s probably best they just went away. Thanks to groups like the Open Web Advocacy and individuals like Shalanah Dawson and Felix Krause for their work and support for this cause.
Recommended Reading
- Shalanah Dawson
- Talk: Solving the In-App Escape Room (This talk at JavaScript MN is what got me curious about all this.)
- Felix Krause
- Open Web Advocacy
- The Meta in-app browser lawsuit
Displaying external sites is not AFAIK the intended use of in-app browsers. Instead they exist to allow developers to author some or even all pages of their app as web pages seamlessly integrated with the rest of the application. That is a perfectly valid use that is not going away.
Exists a couple of sites that has done a good job avoiding in-app browsers, the best that I found, is “getallmylinks”
Maybe this works as an example to understand how they did it. Anyways, thanks for the analysis and explanations.