WASM ile SQLite Veritabanını Tarayıcıda Çalıştırma
Birkaç yıl önce bir müşterimizin projesinde ilginç bir sorunla karşılaştım: kullanıcıların internet bağlantısı olmadan da çalışabilen, veri kaybetmeyen, hızlı bir web uygulaması istiyorlardı. Klasik çözüm IndexedDB ya da localStorage olurdu, ama bu müşteri ciddi SQL sorguları çalıştırmak istiyordu. İşte o noktada WebAssembly üzerinde çalışan SQLite ile tanıştım ve o günden beri bu teknoloji benim araç kutumun vazgeçilmez bir parçası oldu.
Tarayıcıda SQLite mi? Gerçekten mi?
Evet, gerçekten. sql.js ve daha yeni nesil wa-sqlite ya da SQLite WASM Official paketleri sayesinde SQLite’ın tamamını tarayıcı içinde çalıştırmak mümkün. Performans açısından da düşündüğünüzden çok daha iyi sonuçlar alıyorsunuz. Bunun nedeni basit: SQLite zaten son derece optimize edilmiş C kodu, WASM’a derlendikten sonra da bu optimizasyonların büyük kısmı korunuyor.
Peki bu neden önemli? Çünkü bazı senaryolarda sunucuya gitmeden, network gecikmesi olmadan, tamamen istemci tarafında veri işleyebilmek büyük avantaj sağlıyor. Özellikle şu durumları düşünün:
- Offline-first uygulamalar (saha çalışanları, kargo takip sistemleri)
- Hassas veri işleme (verinin sunucuya gitmesini istemediğiniz durumlar)
- Anlık analiz araçları (CSV yükleme ve sorgulama)
- Prototip ve demo uygulamaları
- Edge computing senaryoları
Ortam Hazırlığı
Başlamadan önce ortamı doğru kurmak gerekiyor. Ben genellikle @sqlite.org/sqlite-wasm paketini tercih ediyorum çünkü resmi SQLite ekibinin ürettiği ve aktif olarak geliştirilen bir paket. Ama sql.js de hala çok yaygın kullanılıyor, ikisini de ele alacağım.
# Node.js projeniz için
npm init -y
npm install @sqlite.org/sqlite-wasm
# Ya da sql.js kullanmak istiyorsanız
npm install sql.js
# Basit bir HTTP sunucusu için (WASM dosyaları için gerekli CORS başlıkları)
npm install -D serve
Burada kritik bir nokta var: WASM dosyaları tarayıcıya servis edilirken bazı özel HTTP başlıklarına ihtiyaç duyuyor. Özellikle SharedArrayBuffer kullanan implementasyonlar için Cross-Origin-Opener-Policy ve Cross-Origin-Embedder-Policy başlıkları zorunlu.
# serve.json dosyası oluşturun
cat > serve.json << 'EOF'
{
"headers": [
{
"source": "**/*",
"headers": [
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin"
},
{
"key": "Cross-Origin-Embedder-Policy",
"value": "require-corp"
}
]
}
]
}
EOF
# Sunucuyu başlatın
npx serve -c serve.json .
Eğer Nginx kullanıyorsanız (production senaryolarında yaygın):
# /etc/nginx/sites-available/wasm-app
server {
listen 80;
server_name wasm-app.local;
root /var/www/wasm-app;
location / {
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
add_header Cross-Origin-Resource-Policy "same-origin";
try_files $uri $uri/ /index.html;
}
# WASM dosyaları için doğru MIME type
location ~* .wasm$ {
add_header Content-Type "application/wasm";
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
}
}
sql.js ile Temel Kullanım
sql.js, en yaygın ve belgeleri en geniş olan seçenek. Başlangıç için ideal.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>Tarayıcıda SQLite</title>
</head>
<body>
<div id="output"></div>
<script>
// sql.js'i CDN üzerinden yüklemek isterseniz
// <script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/sql-wasm.js">
async function initDatabase() {
const SQL = await initSqlJs({
// WASM dosyasının konumunu belirtin
locateFile: file => `/static/wasm/${file}`
});
// Bellekte yeni bir veritabanı oluştur
const db = new SQL.Database();
// Tablo oluştur
db.run(`
CREATE TABLE IF NOT EXISTS urunler (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ad TEXT NOT NULL,
fiyat REAL,
stok INTEGER DEFAULT 0,
kategori TEXT,
olusturma_tarihi DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Veri ekle
const stmt = db.prepare(`
INSERT INTO urunler (ad, fiyat, stok, kategori)
VALUES (?, ?, ?, ?)
`);
const urunler = [
["Laptop", 45000.00, 15, "Elektronik"],
["Klavye", 1200.00, 50, "Aksesuar"],
["Mouse", 850.00, 75, "Aksesuar"],
["Monitor", 18000.00, 20, "Elektronik"],
["Webcam", 2500.00, 30, "Aksesuar"]
];
urunler.forEach(urun => {
stmt.run(urun);
});
stmt.free();
// Sorgu çalıştır
const sonuc = db.exec(`
SELECT kategori, COUNT(*) as adet, AVG(fiyat) as ort_fiyat
FROM urunler
GROUP BY kategori
ORDER BY adet DESC
`);
console.log("Sorgu sonucu:", sonuc);
// Veritabanını kapat
// NOT: db.close() çağrılmazsa bellek sızıntısı oluşur!
return { db, sonuc };
}
initDatabase().then(({ sonuc }) => {
document.getElementById('output').textContent =
JSON.stringify(sonuc, null, 2);
});
</script>
</body>
</html>
Resmi SQLite WASM Paketi ile Kalıcı Depolama
sql.js’in en büyük dezavantajı: veritabanını bellekte tutuyor. Sayfa yenilenince her şey gidiyor. Production’da bu yeterli değil. Resmi @sqlite.org/sqlite-wasm paketi ise Origin Private File System (OPFS) API’si ile gerçek kalıcılık sağlıyor.
// sqlite-worker.js - Web Worker içinde çalıştırın
import { default as sqlite3InitModule } from '@sqlite.org/sqlite-wasm';
let db = null;
async function initSQLite() {
const sqlite3 = await sqlite3InitModule({
print: console.log,
printErr: console.error
});
console.log('SQLite sürümü:', sqlite3.version.libVersion);
// OPFS desteği varsa kalıcı, yoksa bellekte tut
if (sqlite3.opfs) {
console.log('OPFS destekleniyor, kalıcı depolama kullanılıyor');
db = new sqlite3.oo1.OpfsDb('/myapp/uygulama.sqlite3');
} else {
console.warn('OPFS desteklenmiyor, bellek içi mod kullanılıyor');
db = new sqlite3.oo1.DB(':memory:');
}
// Şemayı oluştur
db.exec(`
CREATE TABLE IF NOT EXISTS kullanici_aktiviteleri (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kullanici_id TEXT NOT NULL,
eylem TEXT NOT NULL,
detay TEXT,
zaman INTEGER DEFAULT (strftime('%s', 'now')),
senkronize INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_kullanici
ON kullanici_aktiviteleri(kullanici_id);
CREATE INDEX IF NOT EXISTS idx_senkronize
ON kullanici_aktiviteleri(senkronize);
`);
return db;
}
// Worker mesaj dinleyicisi
self.addEventListener('message', async (event) => {
const { type, payload, requestId } = event.data;
try {
if (!db) {
db = await initSQLite();
}
let sonuc;
switch(type) {
case 'QUERY':
sonuc = [];
db.exec({
sql: payload.sql,
bind: payload.params || [],
rowMode: 'object',
callback: (row) => sonuc.push(row)
});
break;
case 'EXEC':
db.exec(payload.sql, { bind: payload.params || [] });
sonuc = { etkilenen: db.changes() };
break;
case 'EXPORT':
// Veritabanını binary olarak dışa aktar
sonuc = sqlite3.capi.sqlite3_js_db_export(db.pointer);
break;
}
self.postMessage({ requestId, success: true, data: sonuc });
} catch (hata) {
self.postMessage({
requestId,
success: false,
error: hata.message
});
}
});
Ana thread’den bu worker’a şu şekilde erişebilirsiniz:
// db-client.js
class SQLiteClient {
constructor() {
this.worker = new Worker('/sqlite-worker.js', { type: 'module' });
this.pendingRequests = new Map();
this.requestCounter = 0;
this.worker.addEventListener('message', (event) => {
const { requestId, success, data, error } = event.data;
const pending = this.pendingRequests.get(requestId);
if (pending) {
this.pendingRequests.delete(requestId);
if (success) {
pending.resolve(data);
} else {
pending.reject(new Error(error));
}
}
});
}
async query(sql, params = []) {
return this._sendMessage('QUERY', { sql, params });
}
async exec(sql, params = []) {
return this._sendMessage('EXEC', { sql, params });
}
_sendMessage(type, payload) {
return new Promise((resolve, reject) => {
const requestId = ++this.requestCounter;
this.pendingRequests.set(requestId, { resolve, reject });
this.worker.postMessage({ type, payload, requestId });
});
}
}
// Kullanım örneği
const db = new SQLiteClient();
async function uygulamayiBaslat() {
// Kayıt ekle
await db.exec(
'INSERT INTO kullanici_aktiviteleri (kullanici_id, eylem, detay) VALUES (?, ?, ?)',
['usr_123', 'LOGIN', JSON.stringify({ ip: '192.168.1.1' })]
);
// Sorgula
const aktiviteler = await db.query(
'SELECT * FROM kullanici_aktiviteleri WHERE kullanici_id = ? ORDER BY zaman DESC LIMIT 10',
['usr_123']
);
console.log('Son aktiviteler:', aktiviteler);
}
Gerçek Dünya Senaryosu: CSV Analiz Aracı
Bu teknolojiyi en etkili gördüğüm kullanım alanlarından biri CSV analizi. Kullanıcı büyük bir CSV dosyası yüklüyor, siz onu SQLite’a aktarıyorsunuz ve SQL ile anlık sorgulama yapabiliyorlar. Sunucuya tek satır veri gitmiyor.
// csv-analyzer.js
async function csvDosyasiniYukle(dosya, db) {
const text = await dosya.text();
const satirlar = text.split('n').filter(s => s.trim());
const basliklar = satirlar[0].split(',').map(h => h.trim().replace(/"/g, ''));
// Dinamik tablo oluştur
const kolonlar = basliklar.map(b =>
`"${b.replace(/[^a-zA-Z0-9_]/g, '_')}" TEXT`
).join(', ');
await db.exec(`DROP TABLE IF EXISTS csv_veri`);
await db.exec(`CREATE TABLE csv_veri (${kolonlar})`);
// Toplu insert için transaction kullan (performans için kritik!)
await db.exec('BEGIN TRANSACTION');
try {
const sorgusablonu = `INSERT INTO csv_veri VALUES (${
basliklar.map(() => '?').join(',')
})`;
let eklenen = 0;
for (let i = 1; i < satirlar.length; i++) {
const degerler = csvSatiriniAyristir(satirlar[i]);
if (degerler.length === basliklar.length) {
await db.exec(sorgusablonu, degerler);
eklenen++;
}
}
await db.exec('COMMIT');
console.log(`${eklenen} satır eklendi`);
return { kolonlar: basliklar, satirSayisi: eklenen };
} catch (hata) {
await db.exec('ROLLBACK');
throw hata;
}
}
function csvSatiriniAyristir(satir) {
const sonuc = [];
let gecici = '';
let tirnakIcinde = false;
for (const karakter of satir) {
if (karakter === '"') {
tirnakIcinde = !tirnakIcinde;
} else if (karakter === ',' && !tirnakIcinde) {
sonuc.push(gecici.trim());
gecici = '';
} else {
gecici += karakter;
}
}
sonuc.push(gecici.trim());
return sonuc;
}
Performans Optimizasyonları
Tarayıcıda SQLite kullanırken dikkat edilmesi gereken bazı kritik noktalar var:
Transaction kullanımı hayati önem taşıyor. Her INSERT için ayrı bir transaction açıp kapamak korkunç yavaş. Bunu bir kez test ettim: 10.000 satır için tek tek insert 45 saniye, tek transaction içinde aynı işlem 800 milisaniye sürdü.
WAL modu OPFS ile birlikte daha iyi performans sağlıyor:
// Veritabanı başlatıldıktan hemen sonra
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA synchronous = NORMAL');
db.exec('PRAGMA cache_size = 10000');
db.exec('PRAGMA temp_store = MEMORY');
db.exec('PRAGMA mmap_size = 268435456'); // 256MB
Bellek yönetimi de önemli bir konu. sql.js’de hazırlanan statement’ları her zaman free() ile serbest bırakın:
// YANLIS - bellek sizintisi
function yanlisSorgula(db, id) {
const stmt = db.prepare('SELECT * FROM urunler WHERE id = ?');
stmt.bind([id]);
return stmt.step() ? stmt.getAsObject() : null;
// stmt.free() unutuldu!
}
// DOGRU
function dogruSorgula(db, id) {
const stmt = db.prepare('SELECT * FROM urunler WHERE id = ?');
try {
stmt.bind([id]);
return stmt.step() ? stmt.getAsObject() : null;
} finally {
stmt.free(); // Her zaman çalışır
}
}
Veritabanını Dışa/İçe Aktarma
Offline uygulamanın kritik ihtiyaçlarından biri: kullanıcının veritabanını yedekleyebilmesi ya da sunucuyla senkronize edebilmesi.
// Veritabanını indir
async function veritabaniniIndir(db) {
// sql.js ile
const veri = db.export(); // Uint8Array döner
const blob = new Blob([veri], { type: 'application/x-sqlite3' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `yedek_${new Date().toISOString().split('T')[0]}.sqlite`;
link.click();
// Belleği serbest bırak
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
// SQLite dosyasını yükle
async function veritabaniniYukle(dosyaInput, SQL) {
const dosya = dosyaInput.files[0];
if (!dosya) return;
const arrayBuffer = await dosya.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// sql.js ile mevcut binary'den DB aç
const db = new SQL.Database(uint8Array);
// Doğrulama sorgusu
try {
db.exec('SELECT count(*) FROM sqlite_master');
console.log('Veritabanı başarıyla yüklendi');
return db;
} catch (hata) {
db.close();
throw new Error('Geçersiz SQLite dosyası');
}
}
// Sunucuyla senkronizasyon (delta sync)
async function sunucuylaSenkronize(db, sunucuUrl) {
// Senkronize edilmemiş kayıtları bul
const bekleyenler = db.exec(`
SELECT * FROM kullanici_aktiviteleri
WHERE senkronize = 0
ORDER BY zaman ASC
LIMIT 100
`);
if (!bekleyenler[0]?.values?.length) {
console.log('Senkronize edilecek veri yok');
return;
}
const yanit = await fetch(`${sunucuUrl}/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kayitlar: bekleyenler[0].values
})
});
if (yanit.ok) {
const { basariliIdler } = await yanit.json();
// Başarılı olanları işaretle
db.run(
`UPDATE kullanici_aktiviteleri
SET senkronize = 1
WHERE id IN (${basariliIdler.map(() => '?').join(',')})`,
basariliIdler
);
}
}
Dikkat Edilmesi Gereken Noktalar
Bu teknolojiyi production’a taşımadan önce bazı gerçekleri göz önünde bulundurmak gerekiyor.
OPFS desteği hala tüm tarayıcılarda tam değil. Safari’nin desteği görece yeni, bazı eski kurumsal tarayıcılarda hiç olmayabilir. Her zaman fallback mekanizması kurun.
SharedArrayBuffer gerektiren implementasyonlar, yukarıda bahsettiğim özel HTTP başlıkları olmadan çalışmaz. Bu başlıklar bazı üçüncü parti widget’ları (reklam, iframe içerikleri) kırabiliyor. Dikkatli değerlendirin.
Veri güvenliği açısından OPFS’te saklanan veriler tarayıcı sandboxında korumalı, ama şifreli değil. Gerçekten hassas veriler için SQLite Encryption Extension veya uygulama seviyesinde şifreleme gerekiyor.
WASM dosyasının boyutu da göz ardı edilemez. sql.js WASM dosyası yaklaşık 1MB civarında. İlk yüklemede bundle boyutunu artırıyor. Lazy loading veya Web Worker ile asenkron yükleme bunu hafifletiyor.
Sonuç
Tarayıcıda SQLite çalıştırmak birkaç yıl önce “neden ki?” sorusunu akla getirirdi. Ama WASM olgunlaştıkça, OPFS API’si yaygınlaştıkça ve offline-first yaklaşım önem kazandıkça bu teknik gerçekten anlamlı hale geldi. Saha operasyonları, analiz araçları, privacy-first uygulamalar, yüksek etkileşimli dashboard’lar gibi alanlarda sunucuya olan bağımlılığı ciddi ölçüde azaltabiliyor.
Resmi @sqlite.org/sqlite-wasm paketinin gelişimi bu ekosistemi daha da sağlam bir zemine oturttu. SQLite ekibinin bizzat ürettiği, aktif geliştirilen ve gerçek kalıcılık sunan bu implementasyon artık production için makul bir tercih.
Siz de projenizde offline veri ihtiyacı veya istemci taraflı analiz gereksinimi varsa, bu teknolojiyi ciddiye almanızı öneririm. Başlangıç için sql.js yeterli, skalayabilir ve production-ready bir şey istiyorsanız OPFS destekli resmi paket doğru tercih. Her iki senaryoda da Web Worker izolasyonu neredeyse zorunlu, ana thread’i bloklamak kullanıcı deneyimini mahveder.
Tarayıcı, artık sadece HTML render eden bir program değil. Ve bu örnekte gördüğümüz gibi, doğru araçlarla ciddi veri işleme yetenekleri kazanabiliyor.
