Hacker News new | past | comments | ask | show | jobs | submit login
Tempted to Abandon React Native for Native Android (kelvinpompey.me)
88 points by silkodyssey on Feb 4, 2017 | hide | past | web | favorite | 61 comments



To summarize this article: The author really enjoyed using React Native up until he hit some performance issues related to loading large images on certain versions of Android.

He doesn't go into what specifically the performance issues were, any techniques he used to profile his app, what third party libraries he tired or anything.

Performance issues happen all the time with native apps that don't use React Native. Optimizing is a core part of what a software engineer does. Giving up on weeks of learning without a good reason seems like a poor choice to me.

As it so happen's I have been working with a colleague of mine who just fixed a React Native performance issue relating to images on Android on older devices. The author might want to check out the following article:

https://facebook.github.io/react-native/docs/performance.htm...

Our particular issue was solved by the following:

"This is especially true when you have text with a transparent background positioned on top of an image, or any other situation where alpha compositing would be required to re-draw the view on each frame. You will find that enabling shouldRasterizeIOS or renderToHardwareTextureAndroid can help with this significantly."

Apart from the above, this article is pretty shallow, which is sad because I would love to read a good article on React Native's performance. Perhaps I will write one if people are interested.


I analysed react-native's performance issues with images and out-of-memory-errors a while back.

Their (and their image library Fresco's) problem is that they make heavy use of object-finalizers, which is an error in Java and especially Android.

I reported a couple of bugs, but got dismissed:

https://github.com/facebook/react-native/issues/8711

https://github.com/facebook/react-native/issues/8780

https://github.com/facebook/fresco/issues/1363

I found this rather frustrating.

I also found out that using plain java-serialization/string over the JS/Java bridge is twice as fast compared to their homegrown memory-management written in C. Again, a bugreport, again, dismissed.

https://github.com/facebook/react-native/issues/10504

It seems that Facebook's Java programmers don't know Java very well.


Fresco is extremely notorious for causing crashes, I had no idea React Native used it. Picasso and Glide are the two sensible choices for image loading.

Facebook's tools for Android are generally written in a way that you get the feeling one could only justify by repeating the manta "We're Facebook, these are Facebook scale problems" the whole time writing you're them. (see: Redex vs just Proguard)

Wether they 're problems worth solving again need not be evaluated.


They're suffering from not-invented-here-syndrome.

Also while custom memory management made sense for Android 2.2, they should have just thrown it out after 3.0.

But giving up code isn't easy.


Redex does tons of things Proguard doesn't, and it operates on Android's native Java-program format, dex files, not java class files like Proguard. What's the problem?


It's diminishing returns, it breaks in many more ways than Proguard with minimal benefit. I truly question if focusing the engineering effort on reducing bloat in their app won't have saved more space than Redex.


"it breaks in many more ways than Proguard" is a phrase I thought I'd never hear


Hey, RN Android dev here: re: object finalizers:

- For Fresco, I think Balazs provided a pretty good response here on how Fresco uses the finalizers (https://github.com/facebook/react-native/issues/8711#issueco...). If you still think there's an issue here, I'll direct Balazs to this post. - For RN, I remember the issue you cited stated that using Object.finalize is 430x slower than having no finalizer, which sounds really bad until you realize we're talking about 430x slower than 5.6ns. Even though we may create thousands of these objects during a typical startup, this cost adds up to a couple of ms across startup -- since we had other ideas for more impactful perf improvement, we didn't take this on, especially since the proposed alternative at the time was to but the onus on developers to remember to manually free these objects. That being said, we've switch to using PhantomReferences internally at FB for this (which is strikes us as a better solution than either of the above), and hopefully will sync that with open source soon.

Re: bridge serialization, I'm sorry that issue fell through the cracks, I'll make sure that we follow up on it internally. I think there's a meta-issue around communication here on issues and PRs, and while there are a ton to follow up on every day, the RN team could be doing better following up on the important ones that need addressing from the team (I think the bridge serialization issue qualifies), and setting proper expectations on all others. I'll follow up on that internally as well.

I apologize on behalf of the team that it's been frustrating -- I promise we do appreciate all the help we get from the community and try to balance working on our internal goals and with the community as best as possible.


Hi, astreet, thanks so much for taking your time,

I want to stress again that finalizers complicate and slows code in very complex ways. Just measuring the startup time isn't enough. You essentially slow down the garbage collector by giving him work and code he can't analyse or optimise.

The mere usage of finalizers, even if they log only, can lead to Out-Of-Memory-errors when there's still memory available!

This user showed that the numbers of waiting finalizers went up to a couple of thousand after they switching to Fresco: https://github.com/facebook/fresco/issues/1363#issuecomment-...

Think about it: 5000 finalizers waiting to be run! The finalizer-queue is single-threaded, which means that every other object on the same ART that has a finalizer will have to wait until these 5000 finalizers are through, before they can be garbage collected.

No need to apologize though, I know that RN is being flooded with attention and changing core code is not trivial.

Again, thank you for your time.


I see, if we're talking about apps somehow accumulating thousands of finalizer references, something seems very wrong, as we're talking about 10's of ms of work. I'm not sure how many references we typically hit in our apps, but I'm pushing the fresco team to A/B test with removing finalizers to actually see what impact we see in our apps: it'd be a good data point to have internally and externally about using finalizers. That being said, we're hoping to update the OSS RN version of fbjni which uses a PhantomReference queue, and Fresco has also been considering a PhantomReference queue. I'll try to keep you updated on the fresco task.


Splendid news, thank you.


Thanks for all the awesome links! The Android portion of React Native is far behind iOS.

"I also found out that using plain java-serialization/string over the JS/Java bridge is twice as fast compared to their homegrown memory-management written in C"

We ran into some issues relating to this when wrapping some native code on Android. I'll pass your links on to my Android colleague.


> Apart from the above, this article is pretty shallow, which is sad because I would love to read a good article on React Native's performance. Perhaps I will write one if people are interested.

Would definitely be interested.


I've been playing a lot with React Native over the last year; professionally for almost 4 months now. This really aligns with my experience. I don't share the same opinion of going back to native code though.

This really is a fantastic platform. You really can, today, write native cross platform and web apps with the same code base. With few exceptions, everything from including native modules to upgrading is painless. The UI metaphor is fantastic, the tooling is superb, the editor support is there, the community is growing. It's a great place to be.

It's not without it's warts though. From my experience, the real problems stem from the 3rd party native modules. It's not even their fault, the platform is just moving so fast. As recently as the 0.40 release, every native module out there was broken on iOS for a short period of time. I was the first to submit PR's to two fairly widely used ones, and that was multiple days after the release. On this point, I believe this is more indicative of a community that generally doesn't upgrade their projects right away- a combination of painful prior experience and a deep seeded distaste of having to re-release their app on all stores as you can leverage a tool like Microsoft Code Push to change the bundle as long as it is compatible with the native shell.

Of the two RN projects I'm working, both have at least one dependency pinned to a Github fork I have of a project, waiting for my patches to be released. This sounds worse than it is, as the majority of native modules are hilariously small and easy to modify, but it still is worth mentioning as a point of friction today. As the community grows, I expect this will diminish.


Yep, exactly my experience. I love React Native but if your not used to the Javascript style of million's dependencies your in for a world of pain.

It's critical to spend more time vetting third party React Native/Javascript libraries as the quality level varies way more than with native libraries.

As a native developer I hadn't spent a lot of time working deeply with npm. One thing to be careful about when saving packages to your project is the use of ^ versus ~. See:

http://stackoverflow.com/questions/22343224/whats-the-differ...

" the tilde matches the most recent minor version (the middle number). ~1.2.3 will match all 1.2.x versions but will miss 1.3.0.

The caret, on the other hand, is more relaxed. It will update you to the most recent major version (the first number). ^1.2.3 will match any 1.x.x release including 1.3.0, but will hold off on 2.0.0."

Considering the volatility of some third party Javascript libraries, this can cause quite a bit of pain.

It's worth doing the following in your home directory add save-prefix=~ to your .npmrc and all npm install's in the future will automatically add ~ instead of ^


I recommend pinning to exact versions and using a tool like npm-check-updates when you want to upgrade to newer versions of libraries. We had a lot of problems with breakage due to different developers having slightly different versions of dependencies.

React Native is a great platform but it's still very fragile, particularly on Android.


I recommend that too. I only used the npm ecosystem for a short time, and still had dozens of silent breakages from libraries which were expected to update in a backward compatible fashion. And even if a dependency itself is pinned to an exact version, it might be that the transitive dependency of this thing changes and breaks. npm shrinkwrap and yarn are supposed to fix that.


facebook actually recommends using yarn instead of npm. it has faster install times.


Yarn is categorically better than npm at dependency management, in addition to its speed. Yarn uses a lock file to pin exact versions no matter what semver range you choose. When you add another dependency, it does not have the side effect of updating other modules. When you run yarn install it downloads the exact package versions described in the lock file rather than the latest version it can get from npm in that range. This ensures you get back to the last working state. Updating modules to the latest in their range is a separate command.


My main gripe is that for practical purposes is all react-native or nothing.

I will be happy if use React ONLY for the UI, and use swift/.net for all the rest. But merge both is problematic (as far I know).

Exist a way to achieve this? Where I can use react only for the design of the UI and fully native elsewhere?


It's fairly straightforward but you need to write some Obj-C, I don't think you can do it entirely in Swift yet. It's pretty painless in iOS, YMMV on Android. See here for details: https://facebook.github.io/react-native/docs/native-modules-...


All due respect, it isn't really the same code base at all. RN assembles your app from a large list of components based on what your JSX file tells it needs to be imported. If you went and counted LOC you'd probably find that most of the lines of code (outside of the RN backbone) live in those modules. The common code you can share is your JSX.


While technically correct it's not very meaningful a distinction in context. I'm not tasked with developing all the code required for every platform, I simply have to describe it once. Native platforms will all run off the exact same JavaScript bundle in a different, platform-specific shell. You wind up with a different bundle if you target the web.


I've done some work with react native, and have definitely been disappointed by the poor quality of many third party libraries that are not supported by Facebook.

This is a big opportunity for those who might wish to make a name for themselves by developing some top quality open source components, but I also think Facebook should do a bit more to make using the phone's core features in a very high quality way as easy as possible. Things like the camera, audio recording, video, etc., are handled only via third party libraries.

The third party open source work is improving, and I'm grateful it exists, but much of it lacks contributors. Many have large lists of unresolved issues on github.

It would be very smart choice for Facebook to put a dozen or so developers on the task of submitting high quality PRs for the top 20 open source non-facebook libraries.


I searched for the holy grail of cross-platform goodness like a fool for years. I now have my first native UI android application on google play and I don't regret the decision. I made a prototype and then optimized it in ways I wasn't able to do in html or react-native. (try downloading and caching pictures rendered in a listview asynchronously while keeping the scroll smooth)

I now don't have an iOS version, but in my opinion having a finished product that is already gaining traction makes up for the lost time of learning to make the same thing for iOS.


There really is no such thing as a free lunch. If you want to skip having to learn the native platforms, you're going to have to put up with your app having at least one giant external dependency in it.

(Native iOS app developer here, dabbled in React Native and went "nope" as soon as I saw how it worked and how heavily it depended on 3rd party components.)


Have you tried FuseTools? https://www.fusetools.com


My next foray into mobile is going to be with this-

https://www.nativescript.org/

Digging through the api it just looks much more capable in directly calling android instead of abstracted cross platform blobs.


I've dabbled with NativeScript a bit. It does seem a bit closer to the underlying platform but from my experience of it, it felt incomplete.

I first tried the Angular version before the final version of Angular was released and I recall occasions where the documentation for NativeScript was behind the actual API. I had to look at the actual code samples to figure out the correct APIs.

The next time I looked at it, I tried the regular JavaScript version and I recall having trouble deciphering the API for the SideBar plugin.

This left me feeling a bit insecure about the product. It looks to have a lot of potential though but it could use a bit more polish and better documentation.


The NativeScript site says that I can use their cross-platform wrappers like Button, which delegates to UIButton on iOS and android.whatever.Button on Android. So far, so good. But suppose I now want to invoke some method on Button that exists only on iOS. Can I do that? If not, it's the least-common-denominator, which I'm not interested in using to build an app. I'm thinking about something like:

var button = new Button(...);

// Set the common properties:

button.backgroundColor = ...;

button.text = ...;

if (iOS) {

  // Set iOS-specific properties
} else {

  // Set Android-specific properties

}


Yes, it literally works like that. See the docs: http://docs.nativescript.org/cookbook/application

The variable is app.ios / app.android


I'm liking the idea of NativeScript as well, though when doing large image work (or audio or video work), you might still want to drop into native code in a plugin.

So that, for instance, you can be sure exactly when the resources for an image have been deallocated.


For the types of problems the author is describing, NativeScript would not be there to support you. NativeScript surfaces the device API's across the JS bridge, and the serialization across this bridge and pressure on it will always lead to less performant code. You would have to drop to native modules all the same, and then what have you solved?


https://docs.nativescript.org/runtimes/android/advanced-topi...

Perhaps I'm misunderstanding your concern, but it doesn't look like js allocates for the actual object just a proxy for the java.


I think you found my concern. When you are in your app code playing with native API's on the JS-side of the bridge, you are playing with mostly thin proxy objects. These proxy objects serialize your intent across the bridge, evaluate, serialize the response, and send it back to you over the bridge.

This is exactly what React Native does too. The difference is how much of this is done. NativeScript encourages you to stay on the JS side of the bridge- that's why they surfaced those API's. React Native encourages you to cross the bridge to do heavy lifting and then bring the final answer back. This means you typically end up utilizing the bridge a lot more for similar features in NativeScript than you would for React Native. Of course, you pay for this in React Native by having to write Native modules. (I'm purposefully ignoring some of the work I've seen in the RN community to surface these API's in the same way that NativeScript does).

At the bottom of that article there are suggestions on how to mitigate some of these problems, and just about all of them relate to minimizing unnecessary trips across the bridge.


I am using FuseTools which is OpenGl. Everything compiles down to C++ or objective C and JavaScript. https://www.fusetools.com/


I've played around with NativeScript a fair bit. It's very, very clever, but I'm not really sure what it gives you beyond just doing native development. Sure, you get to use JavaScript, but you have to write separately for each platform if you break out of their custom UI components, and debugging, testing, etc. etc. is more complicated and annoying that just writing in (my case) Swift and using native tools.


Sounds like a good opportunity to build some performance-tuning muscle :)

Whether using RN or java, you'll want to know how to debug memory/performance issues on Android - they're mostly unavoidable on mobile devices!

(And maybe you can file some issues against React Native, too, which would be great and help the platform)


I've seen large images give native iOS and Android apps trouble. It's a challenging problem to solve no matter what.

And using ReactNative or NativeScript for the high level UI and a plug-in to handle pushing the bits around is probably still a better choice than going completely native.


That's an interesting idea. The code to select and resize the images are native third-party plugins so in this case I am not sure anything more could be done.

I thought maybe the problem was that the app had to switch to another app to select the image. I reimplemented the code using a pure JavaScript plugin based on React Native's CameraRoll API to avoid switching apps and it still crashed when the image was selected for processing.


Just a wild guess - you said opening for processing. Depending on what you're doing with the file and how you're opening it the whole data get's passed over the bridge resulting in some catastrophic memory allocations. I would recommend tapping into the bridge communication and start checking if everything stays where it should. Some picker modules tend to send base64 encoded file beside the URL.


The first module I tried was react-native-image-picker and yes it returns an object with base64 encoded data. What I do with this data is first, resize the image then display the resized image in an ImageView then upload the resized image to Firebase Storage.


You definitely will want to write images to file, then pass the file path over the bridge. That's an easy solution.

I've been using React Native cross platform for over a year, and it has a ridiculous number of issues, but they're trade offs. There are benefits that come with it. React Native is just another tool, evaluate it as you would any other framework.


That's a fair point about the opportunity to build some performance-tuning muscle but I imagine to do that would require solid knowledge of both React and Android. Where do I start? I am more inclined to go towards the native Android side first.

Aa a follow up, I just did a test with a native Android app doing the same thing: getting an image through an Intent. With the native Android app, the same image that was crashing the React Native app doesn't cause any problems.


What does that have to do with performance? You shouldn't serialize hi-rez bitmap and pack it in Intent.

Activities/Intents should at least force you to be careful and not to keep all data around in your memory, but only pass data you really need.


Have you read this:

https://facebook.github.io/react-native/docs/performance.htm...

It might help with some of the performance issues you encountered.

Even if you decide to abandon React Native and just do Native Android or iOS, you are going to have to deal with performance issues at some point in your career.


Are you sending an image bitmap in the actual Intent? I'm surprised you're not having it break when sent to Android's IPC mechanism which has fairly low size limits.

I would use a Content Provider to share the image instead.


It'd be nice with React Native was actually "native" and not just a JS interpreter (really not that much different than HTML5 running in a browser). No wonder there are performance issues.


With ReactNative, the controls are all native, JavaScript is just there for orchestration, so it is far different than HTML 5, given the browser doesn't give you the same array of native components to use.


That's less accurate than my original post, especially given I was commenting on the performance trade-offs of RN's architecture. I highly recommend watching this official video about the internals in order to gain more of an understanding:

https://www.youtube.com/watch?v=8N4f4h6SThc

In short, there aren't too many interpreted languages that don't rely on "native bridges" for things like UI and access to system libraries, but I wouldn't call desktop GUI frameworks for non-native languages like Java Swing or C# WinForms "native" even though they also use what you call "native components."

On the other hand, I would consider iOS Objective-C/Swift, Android NDK, and MFC/QT/GTK+ in compiled languages like C++, Rust, etc. to be truly "native" (but not Android Java, of course).

React "Native" is still very much interpreted code (if anything, RN is a different kind of "browser" that runs JS code -- yes, the JS that runs in browsers still has the same "orchestration" role that you've described from an architecture standpoint).


I haven't been following React closely but this reminded me of a package called SWT that is used as the underpinning for Eclipse (https://www.eclipse.org/swt/). In the case of SWT, the underlying widgets are platform native but the orchestration is in Java. SWT was used to build a few desktop apps but mostly it seems to have been forgotten now. It left me skeptical of these kinds of thin UI layers.


I'm not an expert, but I think that there is a different approach between React / React Native and SWT. SWT created layer of abstractions - when using their apis you were writing code once - and it was supposed to work the same way on all supported platforms. However React / React Native just use the same React paradigm to rendering controls, but the controls themselves are specific to the platforms. What you share is all the code that is not views, but views themselves are separate codes.


I've been working pretty extensively with RN these days.

Almost always, the code is the same (doesn't HAVE to be though but almost always is), and even the views are the same (barring some exceptions where Android and iOS have completely different design language requiring you to customize things at the code level using platform conditionals in the same source file).


If you already know how to do the image related task in Native Android, you could spike a test project and verify that it's performant on the smaller devices and the problem could be attributed to React Native alone. Then it's a matter of porting the high level API to JS.

Given that you believe this is a memory-related problem, perhaps it's because the image is too large to fit into the available memory, and having React Native made it worse.


I did just that. I created a native Android project that selected an image from the image picker and displayed it in an image view. I tested it with a few images and it worked. Even the image that crashed the React Native app.

"Given that you believe this is a memory-related problem, perhaps it's because the image is too large to fit into the available memory, and having React Native made it worse. "

This is precisely what I think it is.


Seems like it can't be that hard to use that native code you wrote in your test in the react-native app though. Write the final image to disk. Load the image in an image view. Never pass the data through the JavaScript bridge.


Maybe this is your opportunity to create "SilkOdyssey's Badass Image Picker for RN, now with less crashy". It seems like you're partway there already. Maybe fork or PR the one you're using.


What do you all think of Cordova? Do the author's comments apply there also? Does it crash from using too much memory after a while of loading objects? And does it make things not work on older devices and platforms making it a dealbreaker for anyone?


My recent experience with Cordova has been much better than I expected. It's quite a bit easier to work with than I thought, and the performance is fine for the relatively 'standard' UI and animations/transitions I needed. And with a few specific tweaks I could even make it seem pretty native (momentum scrolling, etc.).

All that said, I'd still recommend going native or React Native if 1) you can afford it (time/money/properly skilled employee), or if 2) the app's needs are beyond what a 'typical' in-browser web app does.

In regards to the latter I've found quite a number of plugins that let you do notifications, or use the accelerometer, but on the whole I've found that cordova plugins can be finicky. Plus, the more plugins I need to add and the more native functionality I need to use, the more nervous I get about not fully grasping the underlying stuff.

Basically, I used to think Cordova was never really an acceptable option, especially with the release of React Native. Now, I think there are plenty of situations where it's the best solution for a client.


I haven't used Cordova recently, but when I last did a couple of years ago I had similar problems with image handling resulting in memory running out on low spec devices. From the comments I've read here probably for similar reasons as well, the camera capture API was passing a Base64 encoded image to the application rather than a file URL.


I posted a follow-up to this story talking about further explorations based on the feedback I got from your comments.

https://kelvinpompey.me/tempted-to-abandon-react-native-for-...




Applications are open for YC Summer 2019

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: