Skip to content

Hybrid App Approach

A good essay clarifying the concept of hybrid apps can be found in Native vs. Cross-Platform Apps.

The simplest definition of a hybrid app is an app that depends on an embedded browser for functionality. However, this definition is overly simplistic. Below, I propose some more detailed perspectives.

UI Performance

When discussing native apps, my most familiar platform is macOS, where I use Swift, Cocoa, and SwiftUI for app development.

In theory, hybrid apps tend to lag behind native apps in performance due to the intermediate layer between native components and the web container. However, real-world performance often diverges from theory. The flexibility of front-end development can enable hybrid apps to mitigate these performance gaps, while native apps may not always follow best practices.

Consider the simplest text control as an example. I created a document-based app in Xcode using two different UI components: NSTextView (via Storyboard) and Text (via SwiftUI). To test their performance, I opened a large single-line text file (100 MB) and resized the app window, scroll inside to evaluate text-wrapping speed. The intuitive feeling is, the Text component performed worse than the NSTextView component.

This discrepancy arises because SwiftUI exposes more context during view calculations, making it easier for developers to introduce resource-intensive operations inadvertently. By contrast, NSTextView provides more optimization interfaces, such as NSTextStorage and NSTextContainer, which empower developers to fine-tune performance.

For comparison, Apple's TextEdit, an ancient app, offers excellent performance for opening large single-line text files, serving as a benchmark. Another tool, VSCode, can open large text files almost as quickly while providing advanced features like line numbering and a minimap.

Development Efficiency

Thanks to the active front-end ecosystem, hybrid apps provide many productivity-enhancing features, such as Hot Module Replacement (or HMR). Even when testing an app hosted on a remote server, the speed of hybrid app development often surpasses that of native tools.

Communication Performance

The backend process of a web view can migrate to the native program, offering an alternative to web workers for time-consuming tasks with simple input/output requirements. Additionally, tasks involving permission-sensitive operations can be invoked natively and triggered by the web view. However, this raises a critical design challenge: how to structure communication between the web view and the native context effectively.

Data Transfer Performance - Sending messages from the native app to the web view Initially, I implemented data transfer by composing JavaScript code as serialized strings and injecting it into the web view via WKWebView.evaluateJS(). While straightforward, this approach required escaping numerous characters when constructing JavaScript code, making it error-prone.

Later, I practiced the WKUIDelegate delegate method invoked when window.prompt is called in the web view. This method proved to be more efficient than the initial implementation and introduced me to the concept of a "JSBridge."

  • Sending messages from the web view to the native app At first, I used a callback function as the webview.evaluateJS() argument. This callback could receive evaluation results and errors, accommodating various data types like strings and dictionaries. While this method is simple and accessible, it has a notable limitation: the web view cannot actively send messages to the native app.

Eventually, I learned about the web view message handler API, which enables the web view to post messages. By registering a message handler in the web view, this approach streamlined communication and improved efficiency.

Maintainability

The interaction between native code and web components functions like a bridge. As the only coupling point between the native and web layers, this bridge often requires updates whenever new capabilities are added to the web view component. To improve maintainability, modularizing the web view bridge is crucial.

One effective strategy I have practiced the JSON-RPC-based bridge. JSON-RPC, widely used in browser-server communication, offers a structured and consistent protocol for message exchange. For instance, the Language Server Protocol (LSP) adopts a JSON-RPC approach, demonstrating its ability to scale across diverse use cases. Its simplicity and adherence to widely understood standards make it an approach that is both accessible and easily accepted.

Usability

When designing a tool or feature, it's essential to consider its return on investment (or ROI). Resolving this simple question ensures clarity and inspires purposeful development. The guiding principle should be that every return outweighs its investment. Start with a simple demonstration, and refine it into a robust, scalable module.

For web view components, consider these three use cases:

  1. Standalone Browser Support: Provide injected data to enable standalone web page demos, speeding up development and testing.
  2. Native App Integration: Ensure seamless control and display within a native app.
  3. Other Extensions: Adapt the component for integration with VSCode or Chrome, leveraging their safe environments and design practices.

Regardless of the approach, asynchronous web view loading is common ground. It's essential to design mechanisms that deliver consistent performance across use cases.

To maximize adaptability, implement context recognition on the web side rather than the native side, enabling the web component to integrate seamlessly across different environments.