question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Migrate to the JSI & MMKV for better performance

See original GitHub issue

MMKV: 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: image

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:closed
  • Created 4 years ago
  • Reactions:9
  • Comments:10 (2 by maintainers)

github_iconTop GitHub Comments

3reactions
safaiyehcommented, Jul 27, 2021

For anyone that stumbles on this thread a JSI mmkv module exists here: https://github.com/mrousavy/react-native-mmkv

1reaction
ricardobeatcommented, Oct 28, 2020

@Krizzu why was this closed? Seems quite promising for performance (I’m looking at 10s+ of loading stuff from AsyncStorage at this very moment).

Read more comments on GitHub >

github_iconTop Results From Across the Web

I just published react-native-mmkv - An extremely fast ... - Reddit
I just published react-native-mmkv - An extremely fast & synchronous key/value storage created with JSI. It's almost 30x faster than ...
Read more >
react-native-mmkv - npm
Fully synchronous calls, no async/await, no Promises, no Bridge. High performance because everything is written in C++ (even the JS functions ...
Read more >
react-native-mmkv-storage
It employs React Native JSI making it the fastest storage option for React Native. ... and protobuf to encode/decode values to achieve best...
Read more >
React Native JSI/TurboModules pitfalls - Oscar Franco
Using JSI/TurboModules won't necessarily make your module faster. ... where you transfer a lot of data between native and JavaScript.
Read more >
Best Data Storage Option for React Native Apps.
Meet react-native-mmkv-storage, a simple, performance oriented key value storage for react native that supports maps, strings and arrays to ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found