Migrate to the JSI & MMKV for better performance
See original GitHub issueMMKV: cross-platform mobile key/value store made by Tencent https://github.com/Tencent/MMKV JSI: JavaScript interface(?) – its the base of React Native’s new architecture that lets you skip the bridge.
Motivation
The bridge in React Native is really slow, and maintaining your own database is hard & not very fun (for me, at least).
Description
Instead of doing all that, you could just wrap MMKV and use the JSI to skip the bridge. It also makes it synchronous, which is actually good considering how fast it is.
I built this in my app.
Benchmarks of MMKV
from their github repo, as compared to NSUserDefaults
:
I haven’t benchmarked the difference between AsyncStorage
and MMKV
, but my app felt noticeably faster after I switched and that’s all I cared about. It probably is a bad idea if the file size exceeds 100MB
, since its a memory-mapped database (so everything is stored in memory & synced automatically to disk)
New feature implementation
Here’s example code ripped out from my app where I have this working (renamed some things).
In JavaScript, this code is called via:
global.YeetStorage.getItem(key, type)
global.YeetStorage.setItem(key, value, type)
global.YeetStorage.removeItem(key)
//
// StorageModule.m
// yeet
//
// Created by Jarred WSumner on 1/29/20.
// Copyright © 2020 Yeet. All rights reserved.
//
#import "StorageModule.h"
#import <ReactCommon/TurboModule.h>
#import <Foundation/Foundation.h>
#import <MMKV/MMKV.h>
@interface RCTBridge (ext)
- (std::weak_ptr<facebook::react::Instance>)reactInstance;
@end
StorageModule::StorageModule(RCTCxxBridge *bridge)
: bridge_(bridge) {
std::shared_ptr<facebook::react::JSCallInvoker> _jsInvoker = std::make_shared<react::BridgeJSCallInvoker>(bridge.reactInstance);
}
void StorageModule::install(RCTCxxBridge *bridge) {
if (bridge.runtime == nullptr) {
return;
}
jsi::Runtime &runtime = *(jsi::Runtime *)bridge.runtime;
auto reaModuleName = "YeetStorage";
auto reaJsiModule = std::make_shared<StorageModule>(std::move(bridge));
auto object = jsi::Object::createFromHostObject(runtime, reaJsiModule);
runtime.global().setProperty(runtime, reaModuleName, std::move(object));
}
jsi::Value StorageModule::get(jsi::Runtime &runtime, const jsi::PropNameID &name) {
auto methodName = name.utf8(runtime);
if (methodName == "removeItem") {
MMKV *mmkv = [MMKV defaultMMKV];
return jsi::Function::createFromHostFunction(runtime, name, 1, [mmkv](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *arguments,
size_t count) -> jsi::Value {
NSString *key = convertJSIStringToNSString(runtime, arguments[0].asString(runtime));
if (key && key.length > 0) {
[mmkv removeValueForKey:key];
return jsi::Value(true);
} else {
return jsi::Value(false);
}
});
} else if (methodName == "getItem") {
MMKV *mmkv = [MMKV defaultMMKV];
return jsi::Function::createFromHostFunction(runtime, name, 2, [mmkv](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *arguments,
size_t count) -> jsi::Value {
NSString *type = convertJSIStringToNSString(runtime, arguments[1].asString(runtime));
NSString *key = convertJSIStringToNSString(runtime, arguments[0].asString(runtime));
if (!key || ![key length]) {
return jsi::Value::null();
}
if ([type isEqualToString:@"string"]) {
NSString *value = [mmkv getStringForKey:key];
if (value) {
return convertNSStringToJSIString(runtime, value);
} else {
return jsi::Value::null();
}
} else if ([type isEqualToString:@"number"]) {
double value = [mmkv getDoubleForKey:key];
if (value) {
return jsi::Value(value);
} else {
return jsi::Value::null();
}
} else if ([type isEqualToString:@"bool"]) {
BOOL value = [mmkv getBoolForKey:key defaultValue:NO];
return jsi::Value(value == YES ? 1 : 0);
} else {
return jsi::Value::null();
}
});
} else if (methodName == "setItem") {
MMKV *mmkv = [MMKV defaultMMKV];
return jsi::Function::createFromHostFunction(runtime, name, 3, [mmkv](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *arguments,
size_t count) -> jsi::Value {
NSString *type = convertJSIStringToNSString(runtime, arguments[2].asString(runtime));
NSString *key = convertJSIStringToNSString(runtime, arguments[0].asString(runtime));
if (!key || ![key length]) {
return jsi::Value::null();
}
if ([type isEqualToString:@"string"]) {
NSString *value = convertJSIStringToNSString(runtime, arguments[1].asString(runtime));
if ([value length] > 0) {
return jsi::Value([mmkv setString:value forKey:key]);
} else {
return jsi::Value(false);
}
} else if ([type isEqualToString:@"number"]) {
double value = arguments[2].asNumber();
return jsi::Value([mmkv setDouble:value forKey:key]);
} else if ([type isEqualToString:@"bool"]) {
BOOL value = arguments[2].asNumber();
return jsi::Value([mmkv setBool:value forKey:key]);
} else {
return jsi::Value::null();
}
});
}
return jsi::Value::undefined();
}
//
// StorageModule.h
// yeet
//
// Created by Jarred WSumner on 2/6/20.
// Copyright © 2020 Yeet. All rights reserved.
//
#import <jsi/jsi.h>
#include <ReactCommon/BridgeJSCallInvoker.h>
using namespace facebook;
@class RCTCxxBridge;
class JSI_EXPORT StorageModule : public jsi::HostObject {
public:
StorageModule(RCTCxxBridge* bridge);
static void install(RCTCxxBridge *bridge);
/*
* `jsi::HostObject` specific overloads.
*/
jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override;
jsi::Value getOther(jsi::Runtime &runtime, const jsi::PropNameID &name);
private:
RCTCxxBridge* bridge_;
std::shared_ptr<facebook::react::JSCallInvoker> _jsInvoker;
};
You would override setBridge
in the RCTBridgeModule
and call StorageModule::install(self.bridge)
;
There’s a better implementation here too, where you skip the Foundation primitives and use C++ directly. If you did that, you wouldn’t pay the cost of converting from std::string
NSString
.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:9
- Comments:10 (2 by maintainers)
Top GitHub Comments
For anyone that stumbles on this thread a JSI mmkv module exists here: https://github.com/mrousavy/react-native-mmkv
@Krizzu why was this closed? Seems quite promising for performance (I’m looking at 10s+ of loading stuff from AsyncStorage at this very moment).