Hybrid Application: Native-like Experience with WebView — Part 2

Vishal Polley
Tata 1mg Technology
8 min readJul 14, 2022
Hybrid Application using WebViews

In the previous article, we saw how hybrid app comes with numerous benefits such as OTA (Over the air) releases, smaller app bundle size, cost-effectiveness, multi-platform support, etc.

Today, let’s see how we, at Tata 1mg, have architected the hybrid app by utilizing WebViews to provide the users a seamless experience, similar to its native counterpart.

Terminology

In this article, we have used various terms that correspond to web and native apps.

  • Web App — Web pages we open using a browser (Chrome, Mozilla, etc.)
  • Native App — Apps that we install via the application store (Play Store, App Store) written in native languages like Swift, Java, etc.
  • Hybrid App — Part native apps and part web apps.
  • Deeplinks — Sends mobile device users directly to relevant pages in the native app rather than the web browsers.
  • WebView — Embeddable browser that a native application can use to display web content.
  • Bridge — Layer that permits the JavaScript code and the Native code to interact with each other.
  • Event Bus — Communication channel to send and receive data between native and web and vice-versa.
  • Interfaces — Callback methods built around Event Bus that can be invoked from both native and web app sides.

Here we have framed the article, based on the problems we encountered and the solutions we came up with while utilizing WebView for building our hybrid application.

1. Integrating WebView

Problem -
How can we differentiate a normal web page from a WebView page opening in our native application?

Solution -
To solve this, native apps can add certain platform identification headers with the web request. With the help of this, we can identify if the client application has to serve WebView-specific behavior (like hiding header and footer, etc.) or not.

WebView header

Problem -
How can we navigate to and fro within WebView and Native app pages?

Solution -
This can be done with the help of deeplinks. They provide us the functionality to navigate between WebView and Native app pages seamlessly within a native application via implicit/explicit deeplinks.

Navigation in WebViews

Now we know, how to open web-based pages within the native app as WebView pages. Next, let’s see how we can create a common channel (aka. Event Bus) that bridges communication between both ends i.e. WebView to native and native to WebView.

2. Creating an Event Bus

Problem -
Web pages running inside WebView are isolated from the rest of the app. So, how can we establish a communication channel between these entities?

Solution -
Here, WebView APIs offered by various mobile platforms (Android, iOS) can help us as they provide the functionality for communicating and interacting with both platforms (native and web) via a concept called the bridge.

With the help of this bridge, the web-based JavaScript code can invoke and share data with the native app in real-time and vice-versa. Similarly, at the native end, it can listen to the events being invoked via the native bridge.

https://www.kirupa.com/apps/webview.htm
https://www.kirupa.com/apps/webview.htm

At Tata 1mg, we used the above concept, to design and develop a common Event Bus around this bridge. Using which the native apps can communicate with the WebView pages seamlessly. We also made this bus generalized, such that a single interface-based callback is compatible with both the mobile platforms we are currently supporting.

But before talking about the internals, let’s discuss some of the guidelines we followed while developing the interfaces for both the native apps.

Generalizing the Interfaces

While developing the interfaces, we encountered some guidelines that need to be followed by the developers. We tried to group similar methods to a common parent, based on the action they will serve.

Here are a couple of examples -

  • navigation This parent wrapper can take care of methods related to WebView-based navigations like closing the WebView page, toggling the navigation bar, etc.
  • analytics This parent wrapper can take care of all the methods that are related to analytics.
  • clipboard — This parent wrapper can take care of clipboard actions like cut, copy, paste, etc.

So, by defining a common parent wrapper, we have constructed the interface method as -

<parent_wrapper_name><separator><interface_method>(data)

E.g. — navigation.closeWebView(params), clipboard.copy(text) etc.

Here, we have separated the parent and interface method with a dot (.) separator, similarly, we can use any other techniques like camelCase, and PascalCase as a separator.

Now, let’s talk about the internals of generic interface methods (dataLayer) we have designed for data passing and invocation.

a. WebView to Native Communication (postMessage)

WebView to native app communication can be explained as calling the interface methods which are defined in the native environment, from the WebView pages. Using the same Event Bus concept, we can call these methods whenever required from our client application. We have coined these calls as postMessage-based callbacks.

postMessage dataflow
postMessage dataflow

These callbacks lie under the dataLayer parent wrapper, which handles calling and passing relevant data for various interface methods defined at the native end.

postMessage based callbacks

Updating cart details at the native end, getting microphone permissions to perform search operations on WebView pages, etc. are a couple of use cases where postMessage-based callbacks may come in handy.

b. Native to WebView Communication (onMessage)

The native to WebView communication can be achieved by exposing the web-based methods to the native environment using the same Event Bus which exists between them. As soon as the methods are available in the event bus, the native app can invoke these methods, whenever required. We have coined these calls as onMessage-based callbacks.

onMessage dataflow
onMessage dataflow

These callbacks also lie under the dataLayer parent wrapper, which handles calling and passing relevant data for various interface methods, defined at the web end.

onMessage based callbacks

Getting user-related info within WebView from the native, fetching updated location data from the native side, etc. are a couple of use cases where onMessage-based callbacks may come in handy.

Now, let’s discuss some of the performance benefits we were able to achieve by utilizing WebViews.

3. Shredding 3rd Party Analytics Scripts

Problem -
How can we leverage this Event Bus for navigating analytics-based events to the native platform directly?

Solution -
As Event Bus provides the capability to pass data between both apps, we can use this functionality to pass analytics-based information from the web to the native side using the postMessage-based callback interfaces. After the data is received at the native end, the native analytics SDKs can further pass on this data to the relevant analytics services.

Analytics postMessage callback

This gives us some space to conditionally shred the analytics-based scripts for the WebView pages, which helps in reducing network bandwidth consumed and essentially lowers the time to interact (TTI) for the user on the web page.

Next, let’s see how we have made the loaders smart enough, which gives the users a native-like feel when navigating from native to WebView pages and further on.

4. Implementing Smart Loader

Problem -
While navigating from a native page to a WebView page, the load time is significant. How can we improve the user experience?

Solution -
Generally, whenever there is navigation from the native app to the WebView page, it takes some time to fetch the required documents and assets for the web page to open. To show this loading behavior, we used to show the native loading screen till the WebView page is fetched and becomes interactive, as a result blocking the user from interacting with the page.

To solve this, we used the concept of postMessage-based callbacks to remove the native side loader whenever the first byte of the web page document reaches the native side. As a result, the user interaction with the page became much much faster.

WebView Loading Screen: Before vs After
WebView Loading Screen: Before vs After

Furthermore, we used shimmering-based loading screens, giving the users a native-like feel whenever they navigate from a native screen to a WebView-based screen. Let’s talk about it in detail.

Perceived Performance: Shimmer effect + Transitions

To reduce the load-time frustration, we used CSS-based animated skeletons, which act as a placeholder until the contents get loaded, or to visualize content that doesn’t exist yet.

Before animating page-to-page transitions
Before animating page-to-page transitions

Additionally, we used Framer Motion to animate our page transitions and make them smoother. Animating page transitions provide an app-like experience in WebView. We used spring-based transitions supported by framer motion to create a smooth page-to-page transition on WebView pages.

After animating page-to-page transitions
After animating page-to-page transitions

5. Loading WebView smartly via Object-based loading

Problem -
Can caching the assets improve the load time for the WebView page?

Solution -
WebViews use their mechanism of caching which usually doesn’t work when we load the page for the first time which as a result takes a lot of time initially. Along with loading the HTML data, it is used to cache its resources so that if the same page opens up again, the same resources don’t need to be downloaded again and the page can load faster. This works fine, but it has some issues which we wanted to optimize.

Approaches:

  • Using local caching through the file system — This involves downloading the HTML file locally and then listening to a custom domain for caching the images which can again be used.
    As this involves the downloading of HTML before and then changing the domain, this doesn’t help in optimizing the load time.
  • Invoke the WebView activity as Singleton — This approach seems much more promising than the local caching. We can create the singleton for serving the same WebView object, so that cold load time can be optimized. But this would hamper displaying multiple WebViews as this would need reloading.
  • Object pool pattern — This involves keeping an object pool of WebViews which are removed once they are assigned to any view. This also enhances the load-up time of WebView.

Final Words

WebView page’s performance can be as good as your web page’s performance.

Over the past few years, we have invested significantly in improving our web performance, which as a result, has helped us in reducing the load time and enhancing the web experience for our end-users.

Overall, the capabilities of WebView have proved to us, that it can be a great choice for anyone going with hybrid app development.

I hope you enjoyed this blog!

I want to thank Ayush Srivastava, Utkarsh Srivastava, Parth Mahajan, and Manav Prakash for their valuable input.

Do share your feedback and suggestions in the comments section below. Also, feel free to reach out to me on LinkedIn and Twitter.

If you liked this blog, please hit the 👏 .

--

--