Использование решения JSI и сравнение со старой архитектурой с использованием мостов.

Среди способов хранения большего количества данных в приложении React Native у нас есть множество различных библиотек, адаптированных к нашим потребностям. Среди классического асинхронного хранилища (наряду с более новыми аналогами, использующими новую архитектуру, например, react-native-mmkv-store) у нас есть множество решений, таких как Sqlite, RealmDB, WatermelonDB или PouchDB. Однако давайте помнить, что из-за изменения архитектуры у нас есть гораздо более эффективные библиотеки-аналоги, что позволяет значительно ускорить связь между нативной частью и кодом Javascript.

Одной из таких библиотек является react-native-quick-sqlite, которая предоставляет новейшую базу данных Sqlite, а также архитектуру на основе JSI, которая позволяет обмениваться данными без использования собственного мосты. Создатели библиотеки по непонятным мне причинам намеренно не приводят результатов производительности между двумя архитектурами, ссылаясь лишь на аналогичные исследования в случае с базой данных PouchDB (которая, правда, использует предоставленную библиотеку, но добавляет другой уровень абстракции) (статья доступна здесь), где прирост производительности порядка 2~5x, однако из-за создания индексов и оберток это ненадежный исследование на мой взгляд, хотя ускорение производительности заметно и бесспорно. По словам авторов, заметные изменения будут видны только при большем количестве данных, отправленных в базу, что я тоже постараюсь проверить.

В этом посте я хотел бы показать практическое применение библиотеки React Native Quick Sqlite, а также прямую интеграцию с библиотекой TypeORM, добавляя еще один уровень абстракции и позволяя более дружественные операции с базой данных. Мы также сравним данные решения с предыдущей архитектурой и библиотекой react-native-sqlite-storage, проверив реальную разницу в производительности.

Подготовка проекта

Сначала нам нужно подготовить базовый проект, выполнив команду:

npx react-native init quicksqlite

После установки всех зависимостей у нас есть работающее приложение React Native. В моем случае в версии 0.72. В своих экспериментах я буду использовать библиотеку TypeORM и проверять работоспособность на чистом примере. Поэтому я устанавливаю данные зависимости:

yarn add react-native-quick-sqlite typeorm reflect-metadata

Подробнее о зависимостях библиотеки TypeORM можно прочитать здесь: https://github.com/typeorm/typeorm

Чтобы обеспечить интеграцию TypeORM с Quick Sqlite, нам нужно внести некоторые изменения в файл package.json библиотеки TypeORM. Для этого установим библиотеку для добавления изменений в другие библиотеки:

yarn add --dev patch-package

Затем измените файл node_modules/typeorm/package.json:

// ...
"exports": {
    "./package.json": "./package.json", // add this
    ".": {
      "types": "./index.d.ts",
// ...

Затем выполните следующую команду, чтобы не потерять указанное выше изменение при последующих установках пакета:

yarn patch-package --exclude 'nothing' typeorm

Далее нам нужно переименовать библиотеку с помощью плагина babel, который мы устанавливаем с помощью команды (и еще одну библиотеку, позволяющую использовать декораторы):

yarn add babel-plugin-module-resolver @babel/plugin-proposal-decorators -dev

Затем в файле babel.config.js в разделе плагинов добавьте запись:

// ...
['@babel/plugin-proposal-decorators', {legacy: true}],
[
  'module-resolver',
  {
    alias: {
      "react-native-sqlite-storage": "react-native-quick-sqlite"
    },
  },
],
// ...

Чтобы правильно настроить TypeORM, в файле index.js в корневом каталоге проекта нам нужно добавить:

import 'reflect-metadata';

Затем в файле tsconfig.json в разделе compilerOptions нам нужно добавить поддержку декораторов, полезных для создания моделей:

"strict": false,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"skipLibCheck": true

Также не забудьте установить необходимые пакеты iOS, выполнив команду:

npx pod-install

Подключение к базе данных

Для начала нам нужно подключиться к базе данных. Мы создаем новое соединение следующим образом. Класс Person, показанный ниже, дан здесь как сущность.

import {typeORMDriver} from 'react-native-quick-sqlite';
import {DataSource} from 'typeorm';
import {Person} from './Person';

export const dataSource = new DataSource({
  database: 'quicksqlitetest-typeorm.db',
  entities: [Person],
  location: '.',
  logging: [],
  synchronize: true,
  type: 'react-native',
  driver: typeORMDriver,
});

Затем в любом месте нашего приложения нам нужно выполнить метод initialize(), чтобы установить соединение (и создать базу, если она не существует).

useEffect(() => {
  const connect = async () => {
    await dataSource.initialize();
  };

  connect();
}, []);

Модель

Для целей презентации мы создадим простую модель Person, содержащую несколько полей разных типов, где id будет первичным ключом с автоматически сгенерированным значением.

import {Entity, Column, BaseEntity, PrimaryGeneratedColumn} from 'typeorm';

@Entity('people')
export class Person extends BaseEntity {
  @PrimaryGeneratedColumn() id: number;
  @Column('datetime') createdAt: Date;
  @Column('text') firstName: string;
  @Column('text') lastName: string;
  @Column('text') age: number;
}

Простые операции

Для выполнения простых операций над сущностью необходимо создать так называемый репозиторий:

export const PersonRepository = dataSource.getRepository(Person);

Чтобы добавить новый объект, мы можем использовать:

const person = PersonRepository.create({
  createdAt: new Date(),
  firstName: `Person 1`,
  lastName: `Person Lastname 1`,
  age: 10,
}),

await person.save();

Для поиска предметов:

  const peopleSelected = await PersonRepository.find({});

Редактировать:

await PersonRepository.update({}, {lastName: 'test'});

И удалить:

 await PersonRepository.delete({});

Впрочем, вы можете прочитать об этом прямо в документации библиотеки TypeORM, и я бы не хотел подробно обсуждать это здесь.

Производительность

Однако давайте проверим, как это выглядит с точки зрения производительности, и сравним следующие методы:

  • библиотека react-native-quick-sqlite + TypeORM (решение JSI)
  • библиотека react-native-sqlite-storage + TypeORM (старое решение — архитектура мостов)

В данном случае использование TypeOrm позволяет проверить работоспособность библиотек на реальном примере, где комфорт программиста также влияет на конечный продукт.

В обоих случаях были проведены следующие тесты:

  • Добавление 1, 10 и 10000 записей в базу данных.
  • Изменение 1, 10 и 10000 записей из базы.
  • Выбор 1, 10 и 10000 записей из базы.
  • Удаление 1, 10 и 10000 записей из базы.

Каждый тест повторяли 5 раз, и по этим значениям рассчитывали среднее арифметическое.

Тесты проводились на следующих устройствах:

  • iPhone 11 (iOS 16.5)
  • Google Pixel 4 (Android 12)

Результаты приведены ниже. Время в мс отмечено на оси Y.

Краткое содержание

Как и можно было предсказать в случае операций изменения и удаления, между временем выполнения нет разумной разницы — все операции выполняются непосредственно базой данных, и нет необходимости передавать данные между собственным потоком и потоком Javascript. В случае выбрать и добавить дополнительно требуется время для отправки всех данных непосредственно в базу данных Sqlite. В случае архитектуры JSI все работает намного более плавно (в случае архитектуры моста сам мост является узким местом, вызывающим наибольшее снижение производительности). Это сказывается при отправке больших объемов данных, и использование библиотеки с JSI в таких случаях окажет значительное влияние на производительность приложения.

По словам создателей библиотеки, реальное ускорение, особенно для больших наборов данных, передаваемых в базу данных, колеблется от 1,1 до целых 6 раз.

Сам код, необходимый для проведения экспериментов, можно найти здесь: https://github.com/lukaszkurantdev/blog-quick-sqlite-comparison