Tarayıcıda WASM Modül Yükleme ve JavaScript Köprüsü
Bir web uygulaması geliştiriyorsunuz, kullanıcı arayüzü JavaScript ile yazılmış ama performans kritik olan bir bölüm var. Belki görüntü işleme, kriptografi, sıkıştırma algoritması ya da karmaşık matematiksel hesaplamalar. JavaScript’in tek iş parçacıklı yapısı ve JIT derleyicisinin sınırları burada sorun çıkarmaya başlıyor. İşte tam bu noktada WebAssembly devreye giriyor ve “neden bunu daha önce kullanmadım?” dedirtiyor.
Bu yazıda WASM modüllerinin tarayıcıda nasıl yüklendiğini, JavaScript ile nasıl konuştuğunu ve gerçek dünya senaryolarında bu köprünün nasıl kurulduğunu derinlemesine inceleyeceğiz.
WASM Modülü Nedir, Neden Önemli?
WebAssembly, tarayıcıda çalışan düşük seviyeli bir binary format. Rust, C, C++, Go gibi dillerle yazdığınız kodu .wasm uzantılı bir dosyaya derleyip tarayıcıda koşturabiliyorsunuz. Tarayıcı bu binary’yi doğrudan makine koduna yakın bir hızda çalıştırıyor.
Ama şunu net söyleyelim: WASM tek başına hiçbir şey yapamaz. DOM’a erişemez, network isteği atamaz, kullanıcı arayüzüne dokunamaz. Bütün bu işler için JavaScript’e ihtiyacı var. JavaScript ise hesaplama yoğun işler için WASM’a ihtiyaç duyuyor. Bu iki tarafın birbirine bağlandığı noktaya JavaScript Köprüsü diyoruz.
Modül Yükleme Mekanizmaları
Tarayıcıda WASM modülü yüklemek için birkaç farklı yol var. Hangisini seçeceğiniz, kullanım senaryonuza ve performans gereksinimlerinize göre değişiyor.
fetch + WebAssembly.instantiateStreaming
Bu yöntem en verimli ve modern yaklaşım. Modülü ağdan çekerken aynı anda derliyor, belleğe tam olarak indirmeyi beklemiyor.
# Önce basit bir WASM modülü üretelim (Rust örneği)
cargo new --lib wasm-hesap
cd wasm-hesap
// En verimli yükleme yöntemi
async function wasmYukle(url) {
try {
const response = await fetch(url);
// MIME type kontrolü kritik
// Sunucunun application/wasm göndermesi gerekiyor
const { instance, module } = await WebAssembly.instantiateStreaming(
response,
{
// Import objesi - JS'den WASM'a açılan kapılar
env: {
memory: new WebAssembly.Memory({ initial: 256 }),
abort: (msg, file, line, col) => {
console.error(`WASM hata: ${msg} @ ${file}:${line}:${col}`);
}
}
}
);
return instance.exports;
} catch (error) {
console.error('WASM yüklenemedi:', error);
throw error;
}
}
// Kullanım
const wasm = await wasmYukle('/assets/hesap.wasm');
const sonuc = wasm.topla(5, 3);
console.log('Sonuç:', sonuc); // 8
ArrayBuffer ile Manuel Yükleme
Bazı durumlarda modülü önce indirip sonra derlemek isteyebilirsiniz. Service worker cache’inden yüklerken ya da modülü önceden saklayıp sonra kullanmak istediğinizde bu yöntem işe yarıyor.
async function wasmManuelYukle(url) {
// Önce binary'yi indiriyoruz
const response = await fetch(url);
const buffer = await response.arrayBuffer();
// Binary'yi doğrulayalım
const wasmMagic = new Uint8Array(buffer).slice(0, 4);
const beklenen = [0x00, 0x61, 0x73, 0x6d]; // asm
const gecerli = beklenen.every((byte, i) => byte === wasmMagic[i]);
if (!gecerli) {
throw new Error('Geçersiz WASM binary formatı');
}
console.log('WASM binary boyutu:', buffer.byteLength, 'bytes');
// Şimdi derle ve örneklendir
const { instance } = await WebAssembly.instantiate(buffer, {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 512 })
}
});
return instance;
}
Modülü Önce Derle, Sonra Örneklendir
Aynı modülden birden fazla instance oluşturmanız gerektiğinde bu pattern çok işe yarıyor. Worker thread’lere modül kopyalamak ya da farklı konfigürasyonlarla çalıştırmak istediğinizde kullanıyorsunuz.
// Modülü bir kere derle
const wasmModule = await WebAssembly.compileStreaming(
fetch('/assets/goruntu-isleme.wasm')
);
// İstediğin kadar instance oluştur
async function yeniInstance(config) {
const bellek = new WebAssembly.Memory({
initial: config.baslangicSayfalari || 256,
maximum: config.maksimumSayfalari || 1024
});
const instance = await WebAssembly.instantiate(wasmModule, {
env: {
memory: bellek,
log: (ptr, uzunluk) => {
// WASM'dan gelen log mesajlarını okuyoruz
const buffer = new Uint8Array(bellek.buffer, ptr, uzunluk);
const mesaj = new TextDecoder().decode(buffer);
console.log('[WASM]', mesaj);
}
}
});
return { instance, bellek };
}
// Farklı konfigürasyonlarla iki instance
const instance1 = await yeniInstance({ baslangicSayfalari: 128 });
const instance2 = await yeniInstance({ baslangicSayfalari: 512 });
JavaScript Köprüsünü Anlamak
WASM ve JavaScript arasındaki iletişim tek yönlü değil. Her iki taraf da diğerini çağırabilir. Bu köprünün iki kapısı var: Import (JS’den WASM’a) ve Export (WASM’dan JS’e).
Export Kullanımı: WASM’dan JS’e
WASM modülünüzde tanımladığınız fonksiyonlar instance.exports üzerinden erişilebilir hale geliyor.
async function kriptoModuluKur() {
const bellek = new WebAssembly.Memory({ initial: 256 });
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/wasm/kripto.wasm'),
{ env: { memory: bellek } }
);
const exports = instance.exports;
// WASM'dan gelen fonksiyonları kullanalım
return {
// Basit sayısal dönüş - direkt kullanım
sha256Hash: (veri) => {
// String'i WASM belleğine yaz
const encoder = new TextEncoder();
const baytlar = encoder.encode(veri);
// WASM belleğinde yer ayır
const ptr = exports.bellek_ayir(baytlar.length);
// Veriyi belleğe kopyala
const wasmBellek = new Uint8Array(bellek.buffer);
wasmBellek.set(baytlar, ptr);
// Hash hesapla, sonucu al
const sonucPtr = exports.sha256_hesapla(ptr, baytlar.length);
// Sonucu bellek'ten oku (32 byte = SHA256)
const hash = new Uint8Array(bellek.buffer, sonucPtr, 32);
// Belleği temizle
exports.bellek_serbest(ptr);
return Array.from(hash)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
};
}
const kripto = await kriptoModuluKur();
const hash = kripto.sha256Hash('merhaba dünya');
console.log('SHA256:', hash);
Import Kullanımı: JS’den WASM’a
WASM modülü bazen JavaScript tarafındaki özelliklere ihtiyaç duyuyor. Bu durumda import objesi üzerinden fonksiyonları WASM’a geçiriyorsunuz.
// WASM'a JS fonksiyonları geçirme
const importObje = {
env: {
memory: new WebAssembly.Memory({ initial: 256 }),
// WASM'ın log atması için
js_log: (ptr, uzunluk) => {
const baytlar = new Uint8Array(
importObje.env.memory.buffer,
ptr,
uzunluk
);
console.log('[WASM Log]:', new TextDecoder().decode(baytlar));
},
// WASM'ın performans ölçümü yapması için
js_simdi: () => performance.now(),
// WASM'ın rastgele sayı üretmesi için
js_rastgele: () => Math.random(),
// WASM'ın hata fırlatması için
js_hata: (kodPtr, uzunluk) => {
const baytlar = new Uint8Array(
importObje.env.memory.buffer,
kodPtr,
uzunluk
);
throw new Error(new TextDecoder().decode(baytlar));
}
},
// Birden fazla namespace kullanabilirsiniz
matematik: {
sin: Math.sin,
cos: Math.cos,
sqrt: Math.sqrt,
PI: Math.PI
}
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/wasm/fizik-motoru.wasm'),
importObje
);
Bellek Yönetimi: En Kritik Konu
WASM ve JavaScript arasında karmaşık veri alışverişi yaparken bellek yönetimi kritik önem taşıyor. Strings, array’ler, struct’lar doğrudan geçirilemiyor, paylaşılan bellek üzerinden iletişim kuruyorsunuz.
class WasmKoprusu {
constructor(instance, bellek) {
this.instance = instance;
this.bellek = bellek;
this.encoder = new TextEncoder();
this.decoder = new TextDecoder();
}
// JS string'ini WASM'a geçir
stringYaz(metin) {
const baytlar = this.encoder.encode(metin);
const ptr = this.instance.exports.bellek_ayir(baytlar.length + 1);
const hedef = new Uint8Array(this.bellek.buffer, ptr, baytlar.length + 1);
hedef.set(baytlar);
hedef[baytlar.length] = 0; // null terminator
return { ptr, uzunluk: baytlar.length };
}
// WASM'dan string oku
stringOku(ptr, uzunluk) {
const baytlar = new Uint8Array(this.bellek.buffer, ptr, uzunluk);
return this.decoder.decode(baytlar);
}
// Uint8Array geçir
diziYaz(dizi) {
const ptr = this.instance.exports.bellek_ayir(dizi.length);
const hedef = new Uint8Array(this.bellek.buffer, ptr, dizi.length);
hedef.set(dizi);
return ptr;
}
// Float32Array geçir (görüntü işleme için sık kullanılır)
floatDiziYaz(dizi) {
const baytSayisi = dizi.length * 4; // float32 = 4 byte
const ptr = this.instance.exports.bellek_ayir(baytSayisi);
const hedef = new Float32Array(this.bellek.buffer, ptr, dizi.length);
hedef.set(dizi);
return ptr;
}
// Kullanım bittikten sonra belleği temizle
bellek_serbest(ptr) {
this.instance.exports.bellek_serbest(ptr);
}
}
Gerçek Dünya Senaryosu: Görüntü İşleme
Bir e-ticaret sitesinde kullanıcı profil fotoğrafı yüklüyor. Sunucuya göndermeden önce tarayıcıda boyutlandırma ve sıkıştırma yapmak istiyorsunuz. JavaScript’in bu iş için yavaş kaldığı bir senaryo.
class GoruntuIsleyici {
constructor() {
this.hazir = false;
this.kopru = null;
}
async baslat() {
const bellek = new WebAssembly.Memory({
initial: 512, // 32MB başlangıç
maximum: 2048 // 128MB maksimum
});
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/wasm/goruntu.wasm'),
{
env: {
memory: bellek,
js_log: (ptr, len) => {
const baytlar = new Uint8Array(bellek.buffer, ptr, len);
console.log('[Görüntü WASM]', new TextDecoder().decode(baytlar));
}
}
}
);
this.kopru = new WasmKoprusu(instance, bellek);
this.hazir = true;
console.log('Görüntü işleyici hazır');
}
async boyutlandir(imageData, hedefGenislik, hedefYukseklik) {
if (!this.hazir) await this.baslat();
const { data, width, height } = imageData;
// Pixel verilerini WASM belleğine kopyala
const kaynak = new Uint8Array(data.buffer);
const kaynakPtr = this.kopru.diziYaz(kaynak);
// Sonuç için yer ayır
const sonucBoyut = hedefGenislik * hedefYukseklik * 4;
const sonucPtr = this.kopru.instance.exports.bellek_ayir(sonucBoyut);
// WASM'da boyutlandırma yap
const basariKodu = this.kopru.instance.exports.goruntu_boyutlandir(
kaynakPtr, width, height,
sonucPtr, hedefGenislik, hedefYukseklik
);
if (basariKodu !== 0) {
this.kopru.bellek_serbest(kaynakPtr);
this.kopru.bellek_serbest(sonucPtr);
throw new Error(`Boyutlandırma hatası: ${basariKodu}`);
}
// Sonucu oku
const sonucVerisi = new Uint8ClampedArray(
this.kopru.bellek.buffer,
sonucPtr,
sonucBoyut
).slice(); // Kopyala, referans değil
// Belleği temizle
this.kopru.bellek_serbest(kaynakPtr);
this.kopru.bellek_serbest(sonucPtr);
return new ImageData(sonucVerisi, hedefGenislik, hedefYukseklik);
}
}
// Kullanım
const isleyici = new GoruntuIsleyici();
document.getElementById('dosyaSecici').addEventListener('change', async (e) => {
const dosya = e.target.files[0];
const bitmap = await createImageBitmap(dosya);
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
console.time('WASM boyutlandırma');
const kucuk = await isleyici.boyutlandir(imageData, 200, 200);
console.timeEnd('WASM boyutlandırma');
// Sonucu göster
const cikisCanvas = document.getElementById('onizleme');
cikisCanvas.width = 200;
cikisCanvas.height = 200;
cikisCanvas.getContext('2d').putImageData(kucuk, 0, 0);
});
Web Worker ile WASM: Ana Thread’i Boşalt
Büyük hesaplamalar ana thread’i bloklamasın diye WASM’ı Web Worker içinde çalıştırmak en iyi pratik olarak kabul ediliyor.
// wasm-worker.js
let wasmInstance = null;
let bellek = null;
self.onmessage = async (mesaj) => {
const { tip, veri, id } = mesaj.data;
switch (tip) {
case 'BASLAT':
try {
bellek = new WebAssembly.Memory({ initial: 256 });
const { instance } = await WebAssembly.instantiateStreaming(
fetch(veri.url),
{ env: { memory: bellek } }
);
wasmInstance = instance;
self.postMessage({ id, tip: 'HAZIR' });
} catch (hata) {
self.postMessage({ id, tip: 'HATA', mesaj: hata.message });
}
break;
case 'HESAPLA':
if (!wasmInstance) {
self.postMessage({ id, tip: 'HATA', mesaj: 'Modül henüz yüklenmedi' });
return;
}
try {
const baslangic = performance.now();
const sonuc = wasmInstance.exports[veri.fonksiyon](...veri.argumanlar);
const sure = performance.now() - baslangic;
self.postMessage({
id,
tip: 'SONUC',
sonuc,
sure: `${sure.toFixed(2)}ms`
});
} catch (hata) {
self.postMessage({ id, tip: 'HATA', mesaj: hata.message });
}
break;
}
};
// Ana thread kodu
class WasmWorkerKoprusu {
constructor(workerUrl) {
this.worker = new Worker(workerUrl);
this.bekleyenler = new Map();
this.sayac = 0;
this.worker.onmessage = (e) => {
const { id, tip, sonuc, mesaj, sure } = e.data;
const bekleyen = this.bekleyenler.get(id);
if (!bekleyen) return;
this.bekleyenler.delete(id);
if (tip === 'HATA') {
bekleyen.reject(new Error(mesaj));
} else {
bekleyen.resolve({ sonuc, sure });
}
};
}
async baslat(wasmUrl) {
return this._mesajGonder('BASLAT', { url: wasmUrl });
}
async cagir(fonksiyon, ...argumanlar) {
return this._mesajGonder('HESAPLA', { fonksiyon, argumanlar });
}
_mesajGonder(tip, veri) {
const id = ++this.sayac;
return new Promise((resolve, reject) => {
this.bekleyenler.set(id, { resolve, reject });
this.worker.postMessage({ id, tip, veri });
});
}
kapat() {
this.worker.terminate();
}
}
// Kullanım
const kopru = new WasmWorkerKoprusu('/workers/wasm-worker.js');
await kopru.baslat('/wasm/hesap.wasm');
const { sonuc, sure } = await kopru.cagir('karmasik_hesapla', 1000000);
console.log(`Sonuç: ${sonuc}, Süre: ${sure}`);
Yaygın Hatalar ve Çözümleri
Sahada sıklıkla karşılaşılan sorunları ve bunların çözümlerini listeleyelim.
MIME Type Hatası: Sunucunuz application/wasm yerine başka bir MIME type dönüyorsa instantiateStreaming başarısız olur.
# Nginx için WASM MIME type yapılandırması
# /etc/nginx/mime.types dosyasına ekleyin
application/wasm wasm;
# Ya da server bloğuna
location ~* .wasm$ {
add_header Content-Type application/wasm;
# WASM dosyaları için cache ayarı
expires 1y;
add_header Cache-Control "public, immutable";
}
Bellek Sınırı Aşımı: WASM modülü bellek sınırına ulaştığında grow operasyonu başarısız olabilir.
// Bellek büyüme izleme
function bellegiIzle(bellek) {
let oncekiBoyut = bellek.buffer.byteLength;
// Bellek büyüdüğünde buffer referansı değişiyor
// Bu yüzden her zaman bellek.buffer kullanın, referansı saklamayın
setInterval(() => {
const suankiBoyut = bellek.buffer.byteLength;
if (suankiBoyut !== oncekiBoyut) {
console.log(`Bellek büyüdü: ${oncekiBoyut / 1024 / 1024}MB -> ${suankiBoyut / 1024 / 1024}MB`);
oncekiBoyut = suankiBoyut;
}
}, 1000);
}
SharedArrayBuffer Gereklilikleri: Çoklu thread için SharedArrayBuffer kullanmak istiyorsanız COOP ve COEP header’larını ayarlamanız gerekiyor.
# Nginx header yapılandırması
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
Performans İpuçları
Sahada öğrendiğim, kitaplarda yazmayan birkaç pratik ipucu:
- Streaming instantiation kullanın:
instantiateStreamingher zamaninstantiate‘dan daha hızlı. Sadece MIME type doğru olsun. - Modülü önbelleğe alın: Aynı
.wasmdosyasını birden fazla kezfetchetmeyin.WebAssembly.compileStreamingile derleyip saklayın. - Büyük veri transferinden kaçının: JS ile WASM arasında sık sık büyük veri kopyalamak maliyetli. Hesaplamaları mümkün olduğunca WASM tarafında tamamlayın.
- Web Worker kullanın: 100ms üzerinde sürebilecek hesaplamalar için ana thread’i bloklamayın.
- Bellek serbest bırakmayı unutmayın: WASM tarafında GC yok, elle bırakmazsanız leak oluşur.
- Sayfa yenilemeden önce temizlik yapın:
beforeunloadevent’inde worker’ı kapatın ve WASM belleğini serbest bırakın.
Sonuç
WASM ve JavaScript köprüsü ilk bakışta karmaşık görünse de temel prensipleri anladıktan sonra oldukça mantıklı bir yapı. Modül yükleme için instantiateStreaming en iyi seçenek, bellek yönetimi için açık ve tutarlı bir strateji belirleyin, büyük iş yüklerini Web Worker’lara taşıyın.
Gerçek değer ise şurada: JavaScript’in yapamadıklarını WASM yapıyor, WASM’ın yapamadıklarını JavaScript yapıyor. Bu iki teknoloji birbirinin düşmanı değil, tamamlayıcısı. Sysadmin perspektifinden bakıldığında, bu tür uygulamaları deploy ederken MIME type yapılandırması, cache header’ları ve gerekiyorsa COOP/COEP header’larını doğru ayarlamak kritik. Uygulama geliştiricisi bu detayları atladığında masaya gelen ilk kişi yine biz oluyoruz.
Edge computing tarafında ise bu köprü bambaşka bir anlam kazanıyor. Cloudflare Workers, Fastly Compute gibi platformlarda WASM modülleri sunucu olmaksızın edge node’larda çalışıyor, ama bu konu ayrı bir yazının hakkı.
