← cd ../blog

Optimizarea SQLite: De la 2 secunde la 80ms în Spânzurătoarea

Optimizarea SQLite: De la 2 secunde la 80ms în Spânzurătoarea — imagine de copertă

Provocarea: Când baza de date devine gâtul de sticlă

Recent, am lucrat la optimizarea backend-ului local pentru jocul „Spânzurătoarea”. Cu peste 37.000 de cuvinte indexate, incluzând indicii și scoruri de dificultate, aplicația a început să se simtă greoaie. Interogările secvențiale pentru a extrage un cuvânt aleatoriu care să respecte criteriile de dificultate durau uneori peste 2 secunde pe dispozitivele mai vechi. Inacceptabil pentru un joc ce trebuie să fie instantaneu.

Arhitectură: Bucketing vs. Indexare

Inițial, am fost tentat să creez 5 tabele separate pentru fiecare nivel de dificultate. Deși pare o soluție curată, am realizat că ar complica enorm mentenanța și interogările cross-level. Am decis să rămân pe o singură tabelă words, dar să folosesc un index compus pentru a accelera selecția:

CREATE INDEX idx_difficulty_freq ON words(nivel, frecventa);

Acest index a schimbat radical planul de execuție al SQLite. În loc să scaneze întreaga tabelă, motorul sare direct la setul de date relevant pentru nivelul curent.

Trecerea la Drizzle ORM și Type Safety

Migrarea de la expo-sqlite v13 la v15 a venit la pachet cu adoptarea drizzle-orm. Avantajul major? Type safety complet, care previne erorile de runtime în interogările complexe. Iată cum arată schema noastră acum:

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const words = sqliteTable('words', {
  id: integer('id').primaryKey(),
  cuvant: text('cuvant').notNull(),
  nivel: integer('nivel').notNull(),
  frecventa: integer('frecventa').notNull(),
});

Strategia de Caching: Evitarea Repetiției

Pentru a evita ca utilizatorul să vadă același cuvânt de două ori în aceeași sesiune, am implementat un subquery de filtrare care ignoră ID-urile deja vizualizate. Deși subquery-urile pot fi costisitoare, indexul creat anterior a redus impactul la un nivel neglijabil.

const result = await db.select()
  .from(words)
  .where(and(
    eq(words.nivel, targetLevel),
    notInArray(words.id, sessionCache)
  ))
  .limit(1);

Web Fallback: Când SQLite nativ lipsește

Pentru versiunea web a jocului, unde SQLite nativ nu este disponibil, am integrat sql.js folosind un sistem de persistare în localStorage. Aceasta asigură că experiența utilizatorului rămâne unitară, indiferent de platformă:

import initSqlJs from 'sql.js';

const SQL = await initSqlJs({ locateFile: file => `/${file}` });
const db = new SQL.Database(savedData); // Încărcăm din localStorage

Rezultate: Optimizarea în cifre

După implementarea indexării compuse și optimizarea interogărilor prin Drizzle, timpul de răspuns a scăzut de la 2.1s la aproximativ 80ms. Această îmbunătățire de 25x transformă jocul dintr-o experiență frustrantă într-una fluidă, demonstrând că, în mobile development, structura bazei de date este la fel de importantă ca UI-ul propriu-zis.