A performant way to use PouchDB@7 on React Native in 2022

Takuya Matsuyama - May 1 '22 - - Dev Community

Hi, it's Takuya here.
I've been developing a React Native app which syncs user data with CouchDB. The app is called Inkdrop.

I've published several posts on how to use PouchDB on React Native in the past:

This article is an update for using PouchDB on RN.

Fortunately, PouchDB is still maintained, and recently they've shipped v7.3.0 🎉. Much appreciated the community effort.

TL;DR

  • 2~5x speed improvement!
  • Less library tweaks & simpler setup
  • Attachments are no longer supported
  • How to use

Use a JSI-based SQLite driver

Oscar Franco created a great SQLite driver react-native-quick-sqlite, which provides a low-level API to execute SQL queries, fast bindings via JSI. JSI has a smaller overhead than the traditional RN bridge's.
So, this is much faster than react-native-sqlite-2, which I made to get PouchDB to work on RN before.

I wanted to use it in my project, so I made react-native-quick-websql, which is a thin wrapper to make quick-sqlite compatible with WebSQL. It works flawlessly!

I'll discuss how it improves the performance later.

Tackling the NULL character issue (again)

There is still an issue in React Native - It can't store string data with \u0000.

To avoid the issue, the NULL characters are escaped/unescaped in react-native-sqlite-2 like so:



function escapeBlob(data: any) {
  if (typeof data === 'string') {
    return data
      .replace(/\u0002/g, '\u0002\u0002')
      .replace(/\u0001/g, '\u0001\u0002')
      .replace(/\u0000/g, '\u0001\u0001')
  } else {
    return data
  }
}


Enter fullscreen mode Exit fullscreen mode

But it's not ideal because it would affect the performance.

PouchDB internally uses \u0000 for indexing documents with MapReduce in pouchdb-collate:



/ convert the given key to a string that would be appropriate
// for lexical sorting, e.g. within a database, where the
// sorting is the same given by the collate() function.
function toIndexableString(key) {
  var zero = '\u0000';
  key = normalizeKey(key);
  return collationIndex(key) + SEP + indexify(key) + zero;
}


Enter fullscreen mode Exit fullscreen mode

I didn't see why it should be \u0000, so I've changed the zero marker to \u0003:

And it turns out that it works!
Now the overhead of escaping NULL characters has been removed.

It's 2~5x faster 💨

I've tested it on my Google Pixel 5 and it turns out it's blazing fast. Here is a quick benchmark:

sqlite-2 quick-sqlite + quick-websql
Non-cached query for 38 docs 4,510 ms 984 ms
Cached query for 38 docs 275 ms 75 ms
Cached query for 1422 docs 636 ms 206 ms

PouchDB performs MapReduce to index the documents based on design documents.
That's why the first query takes a long time.
But thanks to the small overheads, it's now about 4~5x faster.

I can't wait to release the new version of my app.

Attachments are no longer supported

In the past work, the main focus was to get PouchDB able to store attachments. But I no longer store them in PouchDB, so that requirement has been dropped.
But that allows you to simply use the official PouchDB packages except for pouchdb-collate as I mentioned above.

How to use

Install

Install dev package:



yarn add -D babel-plugin-module-resolver


Enter fullscreen mode Exit fullscreen mode

Install polyfill packages:



yarn add events react-native-get-random-values react-native-quick-base64


Enter fullscreen mode Exit fullscreen mode

Install pouchdb packages:



yarn add pouchdb-core pouchdb-replication pouchdb-mapreduce pouchdb-adapter-http


Enter fullscreen mode Exit fullscreen mode

Install the patched package:



yarn add @craftzdog/pouchdb-collate-react-native


Enter fullscreen mode Exit fullscreen mode

Install storage adapter packages:



yarn add pouchdb-adapter-react-native-sqlite react-native-quick-sqlite react-native-quick-websql


Enter fullscreen mode Exit fullscreen mode
  1. Install CocoaPods:


npx pod-install


Enter fullscreen mode Exit fullscreen mode

Configure

Make a shim.js:



import {shim} from 'react-native-quick-base64'

shim()

// Avoid using node dependent modules
process.browser = true


Enter fullscreen mode Exit fullscreen mode

then, require it at the beginning of your index.js.

Edit your babel.config.js like so:



module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    [
      'module-resolver',
      {
        alias: {
          'pouchdb-collate': '@craftzdog/pouchdb-collate-react-native',
        },
      },
    ],
  ],
}


Enter fullscreen mode Exit fullscreen mode

Initialize

Create pouchdb.ts like so:



import 'react-native-get-random-values'
import PouchDB from 'pouchdb-core'
import HttpPouch from 'pouchdb-adapter-http'
import replication from 'pouchdb-replication'
import mapreduce from 'pouchdb-mapreduce'
import SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite'
import WebSQLite from 'react-native-quick-websql'

const SQLiteAdapter = SQLiteAdapterFactory(WebSQLite)

export default PouchDB.plugin(HttpPouch)
  .plugin(replication)
  .plugin(mapreduce)
  .plugin(SQLiteAdapter)


Enter fullscreen mode Exit fullscreen mode

Then, import and use it as usual:



import PouchDB from './pouchdb'

const db = new PouchDB('mydb.db', {
  adapter: 'react-native-sqlite'
})


Enter fullscreen mode Exit fullscreen mode

That's it!

Limitations & Debugging

As the libraries use JSI for synchronous native methods access, remote debugging (e.g. with Chrome) is no longer possible.
Instead, you should use Flipper.

Hope you find it useful and helpful.


Subscribe Newsletter

My YouTube channel

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .