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: instantiateStreaming her zaman instantiate‘dan daha hızlı. Sadece MIME type doğru olsun.
  • Modülü önbelleğe alın: Aynı .wasm dosyasını birden fazla kez fetch etmeyin. WebAssembly.compileStreaming ile 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: beforeunload event’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ı.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir