React Native JSI Challenge #2
--
Since I wrote about what @ericlewis I and did to get JSI working in React Native 0.59 I’ve been working on the next version of my my navigation and transition library Fluid Transitions that will be using React Native Reanimated as its animation engine (I’m actually writing the new library to be able to use both React Native Animated and Reanimated — just in case).
The next version of Fluid Transitions is built around a base component that automatically interpolates all its style changes. This causes a lot of small animations to be created and some of them can be complex (try adding color interpolation, transforms and run them using springs and you’ll see :)
Background
React Native Reanimated uses the bridge to serialize information about its animations and this comes at a price — combining complex animations can be slower than using React Native Animated (with/without the native driver) — all because of serialization over the bridge.
I decided to see if I could introduce JSI into React Native Reanimated to speed things up. The library pushes nodes describing animations and expressions and they are all evaluated when the React Native UIManager is ready . This is done by adding information to a queue and executing the queued operations when the UIManager
is ready — seems like something that can be used by a JSI binding.
Implementation
I started by cloning the React Native Reanimated repository and adding all the necessary compiler and linker settings in XCode (as described in one of our commits) — basically it’s all about setting some linker flags and fixing header search paths to be able to use the dynamic JSI interfaces.
My idea was to create a JSI module that would live alongside the native module and use the same functions — but this time without going over the bridge.
The binding object
As we’ve seen from the previous post about JSI we need to create a binding class that converts JavaScript calls into C++/Objective-C calls. My binding class looks like this (REAJsiModule.mm
)
class JSI_EXPORT REAJsiModule : public jsi::HostObject {public:
REAJsiModule(REAModule* reaModule);
static void install(REAModule *reaModule);
jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name)
override;private:
REAModule* reamodule_;
};
Note: All code snippets will be displayed with inline code to avoid too many gists — refer to the link in the bottom for the final code.
So what does this class do? This is a simple JSI binding class that wraps calls found in the REAModule
(which is the communication channel between JavaScript and native in Reanimated — go have a look if you’re interested).
The static install
function will install the JSI binding on the current bridge, and the get
method will return all our function wrappers (see the previous challenge for references and explanations).
To install this class we can extend REAModule
(the NativeModule) and perform the installation:
- (void)setBridge:(RCTBridge *)bridge
{
[super setBridge:bridge];...
REAJsiModule::install(self);
}
Remember from the last article that we had some issue installing the JSI binding? This is so much simpler when we’re in a native module — it has a bridge in the setBridge
method! The installation is also rather simple inside our binding:
RCTCxxBridge *cxxBridge = (RCTCxxBridge *)reaModule.bridge;
if (cxxBridge.runtime == nullptr) {
return;
}jsi::Runtime &runtime = *(jsi::Runtime *)cxxBridge.runtime;
auto reaModuleName = “Reanimated”;auto reaJsiModule = std::make_shared<REAJsiModule>(std::move(reaModule));
auto object = jsi::Object::createFromHostObject(runtime, reaJsiModule);
runtime.global().setProperty(runtime, reaModuleName, std::move(object));
Note that we’re checking if the runtime is empty before installing — That’s the snag with installing JSI bindings — the runtime might not alway be available — especially when debugging through Chrome from within VSCode. The rest of the code is only glue for exposing the binding on the bridge.
Exposing a function to JavaScript is done in the get
function:
if (methodName == “dropNode”) {
REAModule* reamodule = reamodule_;
return jsi::Function::createFromHostFunction(runtime, name, 1,
[reamodule](jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *arguments,
size_t count) -> jsi::Value {
auto arg1 = &arguments[0];
[reamodule dropNode:
[NSNumber numberWithDouble:arg1->asNumber()]];
return jsi::Value::undefined();
});
}
Threads and queues
Everything seemed to work fine — but I got some strange crashes in XCode and whenever I see stuff like that I think threading issues. It’s not hard to check which threads are calling what functions. You’ll be able to break in the native code and check the thread panel (remember that when debugging JS in VSCode/Chrome we’ll see the old native module behavior and hence see which functions live on what thread.
I noticed that calls made using the old native module was calling stuff on the UIManager
thread — and used the React Native RCTExecuteOnUIManagerQueue
function to ensure that they’re all being called on the same thread — wether we’re calling function from the JSI or native module.
Wrapping up
So the big question is — did this help on the performance? Yes, kind of… It is not as fast as I’d like it to be — you can still see some lag when running the animations although it feels more snappy. I did some tests and found that using JSI was around 4–5 times faster than using the bridge.
What about Android?
I haven’t had the time to try to do the same in Android — but hopefully someone could help — it should be doable, but it’ll take some time solving all the build issues we’ve already solved under iOS. So this task is still up for grabs!
References
React Native Reanimated: <https://github.com/kmagiera/react-native-reanimated>
JSI Branch/fork: <https://github.com/chrfalch/react-native-reanimated/tree/jsi-methods>
Fluid Transitions (current version): <https://github.com/fram-x/FluidTransitions>