This sounds exactly like Notion’s mobile app tech stack roughly 4 years ago - a web app running in a React Native wrapper webview. We really struggled to get that architecture to perform well, in particular for local caching, although improved React Native bridge performance might make it more feasible these days.
What we noticed is that we’d incur substantial performance bottleneck for anything that needs to move data to/from native because of the number of encode/decode steps and “bridge hops”. For example to read a row from SQLite, count the bridge hops:
Webview JS -> Java: postMessage to read a row from SQLite
Java -> React Native JS: hey, a postMessage happened, what to do?
React Native JS -> Java: please select * from … where …
Java -> C: ok really run this SQL
Then the stack of conversions repeats on the way back to the webview:
The other thing that plagued us performance wise was boot-up speed. At the time (before Hermes JS VM for React Native), we’d have to wait for RN’s JS to boot, figure out our cache status, then boot the webview JS. And then the webview JS would do the bridge dance above to pull data from SQLite to render. Slow - 40 seconds on low end Android slow.
Today we are still mostly a web app, but our wrapper is pure native code. We cold start to a native view and boot the web app in the background. Our throughout & latency to native APIs is substantially faster without the extra bridge hops into and out of the RN JavaScript VM. We managed the original architecture swap from RN -> true native wrapper with a team of three - myself, our first iOS engineer, and our first Android engineer. We do have a large mobile team now though.
One thing I’d add is that deciding to use a webview wrapper is quite common for multiplatform rich text content editors. Google Docs, Dropbox Paper, Quip, Coda, Notion all use this architecture on iOS and Android because implementing an editor is extremely complex. It’s much more expensive to implement an editor 3x than say implementing a few list views and a form 3x.
That's what puzzled me about the article. Why even have React Native in the mix if all you need is a webview and some code to hit native APIs? Is it ease of build/deployment? Developer QOL/familiarity?
I think your way of doing things would provide a better experience for users and I'm not sure what value RN is adding here.
That said I am not a JS or mobile dev so I might be missing something obvious.
I don't know how big the Standard Notes team is. If your team is super small, using React Native as a glorified build system that produces a native app without needing to learn both the Android and iOS toolchains could make some sense. But given that they wrote separate iOS and Android apps before switching to React Native, I think it's more likely to be avoidance of duplicate code in some way by using React Native abstractions over native features via libraries.
An issue we had trying to do that was that we still ended up writing a bunch of platform-specific native code, but instead of doing so in a straight-forward way, we had to fork or vendor abandoned/broken libraries and write React Native flavored native code, then integrate that library into the RN app. If it had an issue, you have a lot more to mentally untangle to debug problems.
That's super interesting, I've read your previous comments about adopting SQLite. With WASM SQLite nearly ready and the origin privet file system API becoming available, do you expect to adopt that in the browser? And if so would you then align the mobile app closer to the web app by sharing that code?
A while back I was working on a notes app, tying to go the single codebase route with Capacitor. The biggest problem I found was with the text selection cursor on iOS. It sits on a layer above all other elements and even appears outside of scrolling overflow, so it would appear over toolbars, really nasty UX. Did you come up against that? As far as I could tell the only solutions were to have a native UI and only use with webview for the editor (that immediately meant writing the UI twice), or create a custom carrot and selection indicator (nasty!).
> do you expect to adopt that [SQLite via Origin-private filesystem] in the browser? And if so would you then align the mobile app closer to the web app by sharing that code?
I'm interested in WASM+SQLite+OPFS. We currently use native SQLite on iOS, Android, as well as in our Electron app for macOS and Windows. WASM+SQLite+OPFS means we can use the same SQLite schema + caching code in the browser that we use on those other platforms, which would align web more with our native apps. We won't go the other way, and try to replace native SQLite with WASM+SQLite+OPFS on iOS or Android, because we have native code on those platforms that also talks to the same DB; on those platforms we've also moved some sync logic from webview into native land so we can sync in the background when the webview may be paused or destroyed. We want to be more native, not more web.
We might consider replacing the native SQLite in Electron with WASM+SQLite+OPFS if the performance looks good, since we don't have any Electron-side code that interacts with the DB.
> The biggest problem I found was with the text selection cursor on iOS. It sits on a layer above all other elements and even appears outside of scrolling overflow, so it would appear over toolbars, really nasty UX. Did you come up against that?
This only occurs inside an `overflow: scroll` container; it doesn't happen if the body itself scrolls. Here's a possibly-related Webkit bug (opened in 2014): https://bugs.webkit.org/show_bug.cgi?id=138201
That means you can overcome with some clever structuring/styling of the DOM, which is how Dropbox Paper for mobile works IIRC. We also found a different work-around in 2018 but it costs too much performance. So, we still have this issue in Notion because our DOM is too tricky to rework to solve this mostly-unimportant issue.
> create a custom carrot and selection indicator (nasty!).
Google Docs does this and it sucks and feels super janky. None of the nice editing gestures work nicely in Docs. Instead of smoothly moving the cursor around via the long-touch -> trackpad gesture, the cursor jumps around in Docs. I'd rather have some weird render issue with the caret drawing over a toolbar at very specific scroll positions, than force my users to use a jank re-implementation of the native selection UX.
Thanks for the detailed response. I've also seen your contributions to CRDT discussions here and it seems Notion is doing lots of super interesting things at the forefront of "local first" collaborative development. (It's a pity you only hire in the US!)
Yes, I found the "overflow: auto" issue with WebKit, unfortunately even scrolling the whole page doesn't solve the cursor above a "position: fixed" toolbar... hopefully one day Safari on iOS will catch up!
> our wrapper is pure native code. We cold start to a native view and boot the web app in the background. Our throughout & latency to native APIs is substantially faster without the extra bridge hops into and out of the RN JavaScript VM. We managed the original architecture swap from RN -> true native wrapper
Does this mean you boot the web app in a native WebView in the background (not visible), while showing a [splash?] screen until the web-app sends a signal from the web-view to java (or swift?) that it's ready?
Or have you started to build out native-views too? eg. Splash screen, some other super top level screens, etc?
> Does this mean you boot the web app in a native WebView in the background (not visible), while showing a [splash?] screen until the web-app sends a signal from the web-view to java (or swift?) that it's ready?
Yeah, that's what the RN codebase did, and for our initial switch over to native, we did the same thing there too.
These days we launch into a native "Home tab" view that lists all the pages in the workspace, as well as recently edited pages. Our philosophy is to progressively convert views to native, working from the outside in towards the editor. Soon we'll be launching a beta for native-ifying another of our top-level tabs.
Cool, do you try to make it a seamless transition to native with the web views matching styles, etc.? Or do users know which sections are native versus not based on appearance or interactivity (slower)?
I occassionally use the Notion Android app - and you can feel the difference in the native interface and the webview in this case. But only due to the interactivity - appearance is similar and cohesive.
My experience wasn't good before the introduction of the native home tab - I experienced high loading times right on opening the app and degraded usability - which made me quit from that page most of the time. Opening a page from the home tab now still has delay associated and the transition is not yet seamless (for me - I get a blank screen and then the page starts to load). But the home tab helps significantly improve the overall experience as the initial wait time is no longer present. Like mentioned the experience is improving - and I'm looking forward to the beta with more native interfaces!
Huh. At work, I did the same for a WYSIWYG editor we had to implement, but it always felt wrong for me. After looking for a native editor library I was surprised how little I could find.
I can sleep a little better knowing that even the heavy hitters resort to webview sometimes
Even the first iPhone OS used WebView for all text inputs before they implemented "native" components. Literally, every text control was a WebView. Turns out they are really great for text editing :)
Where did you get that impression? Do you have a source?
A WebView may or may not be great for text editing, but it'd definitely be overkill for simple text input. The first iPhone was severely resource constrained, I can't imagine it had the luxury of using a WebView for each and every text input.
Of course, it was not the current multiprocess WKWebView, which requires more resources, and not the legacy UIWebView directly, more likely some of its internals.
> Web browsing on the iPhone was always on the feature list, so WebKit would be there, and the editing code came along with it. We needed to decide how other styled text would work, like in Notes. Should we bring over the AppKit text system?
> Eventually, we decided not to. We were so pressed for memory that fitting two styled text systems was judged too much, so I used WebKit to back UITextField and UITextView. I’m pretty sure it stayed that way until iOS 7.
This is an architecture I’ve noticed most (if not all?) large cross-platform mobile apps land on: a native core to quickly get the user to an interactive state and have smooth navigation, that then gives way to some kind of web technology for the actual feature screens.
What we noticed is that we’d incur substantial performance bottleneck for anything that needs to move data to/from native because of the number of encode/decode steps and “bridge hops”. For example to read a row from SQLite, count the bridge hops:
Webview JS -> Java: postMessage to read a row from SQLite
Java -> React Native JS: hey, a postMessage happened, what to do?
React Native JS -> Java: please select * from … where …
Java -> C: ok really run this SQL
Then the stack of conversions repeats on the way back to the webview:
SQL C -> Java -> RN JavaScript -> Java -> Webview JavaScript
The other thing that plagued us performance wise was boot-up speed. At the time (before Hermes JS VM for React Native), we’d have to wait for RN’s JS to boot, figure out our cache status, then boot the webview JS. And then the webview JS would do the bridge dance above to pull data from SQLite to render. Slow - 40 seconds on low end Android slow.
Today we are still mostly a web app, but our wrapper is pure native code. We cold start to a native view and boot the web app in the background. Our throughout & latency to native APIs is substantially faster without the extra bridge hops into and out of the RN JavaScript VM. We managed the original architecture swap from RN -> true native wrapper with a team of three - myself, our first iOS engineer, and our first Android engineer. We do have a large mobile team now though.
There’s some more FAQs and answers about this on Twitter here: https://twitter.com/jitl/status/1530326516013342723?s=46&t=x...
One thing I’d add is that deciding to use a webview wrapper is quite common for multiplatform rich text content editors. Google Docs, Dropbox Paper, Quip, Coda, Notion all use this architecture on iOS and Android because implementing an editor is extremely complex. It’s much more expensive to implement an editor 3x than say implementing a few list views and a form 3x.