Menyimpan data semi-terstruktur dalam kolom berformat JSON sering menjadi pilihan arsitektur offline-first di React Native. Namun, memfilter data langsung dari dokumen JSON menggunakan fungsi seperti json_extract() pada tabel berisi puluhan ribu baris memicu degradasi performa drastis yang berakibat langsung pada UI jank.
Akar Masalah: Bottleneck json_extract() dan UI Jank
Ketika menjalankan query filter terhadap payload JSON tanpa indeks struktural, mesin SQLite harus memproses baris demi baris:
SELECT id, payload FROM offline_events
WHERE json_extract(payload, '$.metadata.userId') = 'usr_1092';Query tersebut menyebabkan dua masalah utama:
- Full Table Scan (SCAN TABLE): SQLite memeriksa setiap baris data dari disk satu per satu karena tidak dapat mengindeks teks mentah JSON secara langsung.
- Parsing Berulang pada Thread Eksekusi: Setiap baris memaksa mesin SQLite mem-parse representasi string teks JSON menjadi token AST internal hanya untuk membaca satu nilai kunci. Jika terdapat 50.000 baris, parser JSON dijalankan 50.000 kali per eksekusi query.
Pada React Native, SQLite umumnya berjalan di native thread atau JS thread via bridge/JSI (misalnya melalui library seperti op-sqlite atau expo-sqlite). Pemanfaatan CPU 100% untuk parsing string secara intensif akan memblokir komunikasi JSI dan penjadwalan frame rendering, memicu penurunan frame rate ke bawah 60 FPS.
Diagnosa Eksekusi Menggunakan EXPLAIN QUERY PLAN
Gunakan perintah EXPLAIN QUERY PLAN untuk memverifikasi jalur eksekusi query pada SQLite:
EXPLAIN QUERY PLAN
SELECT id, payload FROM offline_events
WHERE json_extract(payload, '$.metadata.userId') = 'usr_1092';Output diagnosa awal:
QUERY PLAN
`--SCAN offline_eventsLabel SCAN offline_events menandakan algoritma pencarian linear berkos O(N). Kompleksitas waktu dan komputasi akan melonjak linier seiring bertambahnya data offline di perangkat pengguna.
Solusi: Indeks Generated Column
SQLite (versi 3.31.0 ke atas) mendukung Generated Column (dikenal juga sebagai computed column). Mekanisme ini mengekspos properti spesifik di dalam dokumen JSON sebagai kolom virtual atau tersimpan yang dapat dipasangkan dengan B-Tree index standar.
VIRTUAL vs STORED
Terdapat dua tipe Generated Column di SQLite:
- VIRTUAL: Nilai kolom dihitung secara dinamis saat dibaca dan tidak memakan ruang penyimpanan tambahan pada skema tabel mentah. Saat dipasangkan dengan B-Tree Index, nilai yang diekstraksi disimpan di dalam struktur B-Tree itu sendiri, bukan di baris tabel data utama.
- STORED: Nilai dievaluasi saat baris di-insert atau di-update, kemudian disimpan langsung secara persisten di disk bersama kolom tabel lainnya. Ini meningkatkan beban penulisan I/O disk dan ukuran file database lokal.
Di platform mobile dengan keterbatasan penyimpanan storage flash, tipe VIRTUAL yang dipasangkan dengan B-Tree index adalah pilihan optimal. Indeks hanya menduplikasi nilai kunci yang dicari tanpa menduplikasi data di tabel utama.
Implementasi Skema SQL dan Query
Definisikan tabel dengan generated column bertipe VIRTUAL, lalu buat indeks terpisah:
CREATE TABLE offline_events (
id TEXT PRIMARY KEY NOT NULL,
payload TEXT NOT NULL,
user_id TEXT GENERATED ALWAYS AS (json_extract(payload, '$.metadata.userId')) VIRTUAL
);
-- Buat B-Tree Index pada generated column
CREATE INDEX idx_offline_events_user_id ON offline_events(user_id);Jalankan kembali EXPLAIN QUERY PLAN:
EXPLAIN QUERY PLAN
SELECT id, payload FROM offline_events
WHERE user_id = 'usr_1092';Output diagnosa setelah indexing:
QUERY PLAN
`--SEARCH offline_events USING INDEX idx_offline_events_user_id (user_id=?)Status berubah dari SCAN menjadi SEARCH TABLE ... USING INDEX. Kompleksitas pencarian turun drastis dari O(N) menjadi O(log N).
Perbandingan Latensi dan Penggunaan Sumber Daya
Pengujian pada dataset 50.000 baris JSON terkompresi dengan ukuran file database ~45MB di lingkungan simulasi mobile menghasilkan data performa berikut:
- Unindexed json_extract() (Full Scan): Latensi rata-rata ~140ms - 260ms per query. Utilisasi CPU native thread melonjak hingga 95-100%, memicu frame drop nyata pada transisi layar React Native.
- Indexed Virtual Column (B-Tree Search): Latensi rata-rata turun drastis menjadi ~0.8ms - 3ms. Beban CPU berada di bawah 5%, eliminasi komputasi parsing JSON secara total pada fase pembacaan data.
Analisis Trade-off: Storage vs CPU
Penerapan Generated Column memiliki trade-off yang perlu disesuaikan dengan kebutuhan aplikasi:
- Overhead Storage Indeks: B-Tree index menempati ruang storage fisik tambahan pada perangkat (rata-rata 10-25% dari ukuran total kolom yang diindeks). Batasi pembuatan indeks hanya untuk atribut JSON yang sering digunakan pada filter
WHEREatauORDER BY. - Overhead Write/Insert: Penambahan indeks menambah beban algoritma balancing pohon B-Tree saat data baru dimasukkan. Gunakan transaksi (
BEGIN TRANSACTION/COMMIT) untuk operasi batch insert data sinkronisasi agar I/O disk tetap minimal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!