WebAssembly Modülünü Küçültme ve Yükleme Hızını Artırma

Geliştirme ortamında her şey güzel çalışıyor, testler yeşil, deployment tamamlandı. Sonra production’a bakıyorsunuz: kullanıcılar “sayfa yüklenmiyor” diye şikayet ediyor, 4MB’lık bir WASM dosyası ağır ağır indirilmeye çalışıyor ve mobil kullanıcılar sayfayı çoktan terk etmiş. Bu senaryo benim başıma geldi ve o günden beri WASM optimizasyonu benim için bir obsesyona dönüştü.

WebAssembly, özellikle edge computing senaryolarında inanılmaz güçlü bir araç. Ama “çalışıyor” ile “iyi çalışıyor” arasındaki fark, özellikle dosya boyutu ve yükleme hızı konusunda çok kritik. Bu yazıda gerçek dünyadan öğrendiğim teknikleri, araçları ve bazı acı deneyimleri paylaşacağım.

WASM Dosya Boyutu Neden Bu Kadar Önemli?

Edge computing ortamlarında, özellikle Cloudflare Workers veya Fastly Compute@Edge gibi platformlarda, WASM modülünüzün boyutu sadece kullanıcı deneyimini değil, cold start sürelerini ve hatta fatura tutarınızı doğrudan etkiliyor. Bir Workers ortamında her request için modülün instantiate edilmesi gerekebiliyor ve bu süre boyut ile doğru orantılı.

Normal bir web uygulaması için de durum farklı değil. Kullanıcının ilk kez sitenize geldiğinde browser cache’inde hiçbir şey yok. O 4MB’lık dosya indirilene kadar JavaScript tarafı bekliyor, spinner dönüyor, kullanıcı sabırsızlanıyor.

Hedefimiz basit: Mümkün olan en küçük WASM dosyasını, mümkün olan en hızlı şekilde kullanıcıya ulaştırmak.

Build Optimizasyonu: Temelden Başlayın

Önce Rust ile yazılmış bir WASM modülü üzerinden gidelim, çünkü Rust ekosistemi bu konuda en olgun araçlara sahip.

Cargo.toml Optimizasyonları

[profile.release]
opt-level = "z"        # Boyut odaklı optimizasyon ("s" de kullanılabilir)
lto = true             # Link Time Optimization
codegen-units = 1      # Paralel derleme kapatılır, daha küçük çıktı
panic = "abort"        # Unwind tabloları olmadan, önemli boyut kazancı
strip = true           # Debug sembollerini sil

[profile.release.package."*"]
opt-level = "z"

Bu ayarlar tek başına genellikle %20-40 boyut küçültmesi sağlıyor. panic = "abort" özellikle önemli: varsayılan unwind modu stack unwinding için ciddi miktarda kod ekliyor.

wasm-opt ile Post-Processing

Binaryen’in wasm-opt aracı, WASM bytecode üzerinde agresif optimizasyonlar yapıyor. Bu araç olmadan production’a çıkmayın.

# Önce wasm-opt'u kurun
npm install -g wasm-opt
# veya
cargo install wasm-pack

# Temel optimizasyon (boyut odaklı)
wasm-opt -Oz input.wasm -o output.wasm

# Maksimum agresif optimizasyon
wasm-opt -O4 --strip-debug --strip-producers 
  --zero-filled-memory 
  input.wasm -o output.wasm

# Boyut karşılaştırması
ls -lh input.wasm output.wasm

Ben genellikle -Oz flag’ini kullanıyorum. -O4 teorik olarak daha fazla optimizasyon yapıyor ama derleme süresi dramatik biçimde artıyor ve bazen edge case’lerde beklenmedik davranışlar görebiliyorsunuz. Production’da güvenlik her zaman önce gelir.

wasm-pack ile Entegre Build Pipeline

# Development build
wasm-pack build --target web --dev

# Production build - tüm optimizasyonlar devrede
wasm-pack build --target web --release -- --features wee_alloc

# Belirli bir target için
wasm-pack build --target bundler --release
wasm-pack build --target nodejs --release

wee_alloc özelliğini ayrıca belirtmek lazım. Rust’ın standart allocator’ı sistemden sistem allocator’ını kullanır ve WASM binary’sine ciddi overhead ekler. wee_alloc ise WASM için optimize edilmiş, çok daha küçük bir allocator.

[dependencies]
wee_alloc = { version = "0.4", optional = true }

[features]
default = ["wee_alloc"]
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

Gereksiz Bağımlılıkları Temizlemek

Bu konu çok konuşulmuyor ama bence en etkili optimizasyon tekniklerinden biri. cargo bloat aracı ile binary’nizdeki her fonksiyonun ne kadar yer kapladığını görebilirsiniz.

# cargo bloat kurulumu
cargo install cargo-bloat

# WASM target için analiz
cargo bloat --release --target wasm32-unknown-unknown -n 20

# Crate bazlı analiz
cargo bloat --release --target wasm32-unknown-unknown --crates

Gerçek bir projede bu analizi yaptığımda şoke oldum: kullandığım bir HTTP kütüphanesinin sadece error message formatting’i için binary’e 200KB’dan fazla string literal eklediğini gördüm. Production WASM modülünde detaylı error mesajlarına ihtiyacınız yok, hata kodları yeterli.

// Yerine
return Err("Unexpected token at position 42 in the JSON parser");

// Bunu kullanın
return Err(ErrorCode::UnexpectedToken);

Compression: Boyutu Sunucu Tarafında Küçültmek

Build-time optimizasyonunuzu yaptıktan sonra, network üzerinden gönderilen boyutu daha da küçültmek için compression kullanmanız şart.

# Brotli compression (WASM için ideal)
brotli -q 11 module.wasm -o module.wasm.br

# Gzip alternatifi
gzip -9 module.wasm

# Boyut karşılaştırması
echo "Orijinal: $(ls -lh module.wasm | awk '{print $5}')"
echo "Brotli: $(ls -lh module.wasm.br | awk '{print $5}')"
echo "Gzip: $(ls -lh module.wasm.gz | awk '{print $5}')"

Nginx konfigürasyonunda static olarak compressed dosyaları sunmak:

# nginx.conf
server {
    location ~* .wasm$ {
        gzip_static on;
        brotli_static on;
        
        add_header Content-Type application/wasm;
        add_header Cache-Control "public, max-age=31536000, immutable";
        
        # CORS headers (gerekirse)
        add_header Cross-Origin-Embedder-Policy require-corp;
        add_header Cross-Origin-Opener-Policy same-origin;
    }
}

Brotli, özellikle WASM bytecode üzerinde Gzip’ten %20-30 daha iyi sıkıştırma oranı elde ediyor. Eğer sunucunuz Brotli destekliyorsa kesinlikle kullanın.

Streaming Instantiation: Yükleme Hızını Dramatik Artırın

Bu teknik, fark yaratan en önemli optimizasyonlardan biri. Standart yaklaşımda şöyle bir şey görürsünüz:

// YANLIŞ YAKLAŞIM - Bütün dosya inmeden instantiate edilemiyor
fetch('/module.wasm')
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes))
  .then(result => {
    // Kullan
  });

Bu yaklaşımda browser önce tüm dosyayı indiriyor, sonra buffer’a alıyor, sonra compile ediyor, sonra instantiate ediyor. Seri işlemler.

// DOĞRU YAKLAŞIM - WebAssembly.instantiateStreaming
async function loadWasmModule(url) {
  try {
    const { instance, module } = await WebAssembly.instantiateStreaming(
      fetch(url, { 
        headers: { 'Accept': 'application/wasm' }
      }),
      importObject
    );
    return instance;
  } catch (err) {
    // Streaming desteklenmiyorsa fallback
    console.warn('Streaming instantiation desteklenmiyor, fallback kullanılıyor');
    const response = await fetch(url);
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, importObject);
    return instance;
  }
}

instantiateStreaming ile browser, dosya indirilirken aynı anda compile işlemini başlatabiliyor. Bu, büyük modüllerde yükleme süresini %50’ye kadar düşürebiliyor.

Kritik nokta: instantiateStreaming çalışması için sunucunuzun Content-Type: application/wasm header’ını doğru göndermesi gerekiyor. Bu header eksikse browser streaming kullanamıyor.

Module Caching: Compile Maliyetini Bir Kere Öde

WASM compile işlemi pahalıdır. Kullanıcı her sayfa yenilemesinde bunu tekrar yapmasın diye compiled module’ü cache’leyebilirsiniz.

const CACHE_NAME = 'wasm-modules-v1';

async function getCachedWasmModule(url) {
  // Cache API ile compiled module cache'leme
  if ('caches' in window) {
    const cache = await caches.open(CACHE_NAME);
    const cachedResponse = await cache.match(url);
    
    if (cachedResponse) {
      const bytes = await cachedResponse.arrayBuffer();
      // Compile sonucunu IndexedDB'de tut
      return await WebAssembly.compile(bytes);
    }
  }
  
  // Cache yoksa fresh yükle
  const response = await fetch(url);
  const clone = response.clone();
  
  // Background'da cache'e ekle
  if ('caches' in window) {
    const cache = await caches.open(CACHE_NAME);
    cache.put(url, clone);
  }
  
  return await WebAssembly.compileStreaming(response);
}

// IndexedDB ile compiled WebAssembly.Module cache'leme
async function storeCompiledModule(key, module) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('WasmCache', 1);
    
    request.onupgradeneeded = (event) => {
      event.target.result.createObjectStore('modules');
    };
    
    request.onsuccess = (event) => {
      const db = event.target.result;
      const tx = db.transaction('modules', 'readwrite');
      tx.objectStore('modules').put(module, key);
      tx.oncomplete = resolve;
      tx.onerror = reject;
    };
  });
}

Lazy Loading ve Code Splitting

Her şeyi tek bir WASM modülüne koymak zorunda değilsiniz. Kullanıcının başlangıçta ihtiyaç duymadığı fonksiyonları ayrı bir modüle taşıyabilirsiniz.

// Ana modül hemen yüklenir
const coreModule = await WebAssembly.instantiateStreaming(
  fetch('/wasm/core.wasm')
);

// Ağır işlem modülü sadece gerektiğinde yüklenir
let heavyModule = null;

async function runHeavyComputation(data) {
  if (!heavyModule) {
    console.log('Ağır modül yükleniyor...');
    heavyModule = await WebAssembly.instantiateStreaming(
      fetch('/wasm/heavy-computation.wasm')
    );
  }
  
  return heavyModule.instance.exports.process(data);
}

// Kullanıcı bu butona bastığında modül yüklenir
document.getElementById('compute-btn').addEventListener('click', async () => {
  const result = await runHeavyComputation(inputData);
  displayResult(result);
});

Bu pattern özellikle PDF render, video processing veya karmaşık matematik hesapları gibi ağır işlemler için mükemmel. Kullanıcıların büyük bir kısmı bu özelliklere hiç dokunmayacak, neden hepsine o kodu indirteyim?

Edge Computing Özel Optimizasyonları

Cloudflare Workers gibi ortamlarda WASM kullanıyorsanız, bazı ek kısıtlamalar var.

# Wrangler ile Workers bundle boyutu analizi
npx wrangler deploy --dry-run --outdir dist

# Workers için WASM boyut limiti kontrolü
ls -lh dist/*.wasm
// Cloudflare Workers'da WASM kullanımı
import wasmModule from './module.wasm';

export default {
  async fetch(request, env, ctx) {
    // Workers'da module-level instantiation kullanın
    // Her request'te yeniden instantiate etmeyin
    const instance = await WebAssembly.instantiate(wasmModule);
    
    const url = new URL(request.url);
    const input = url.searchParams.get('data');
    
    if (!input) {
      return new Response('data parametresi gerekli', { status: 400 });
    }
    
    const result = instance.exports.process(
      Buffer.from(input).buffer
    );
    
    return new Response(JSON.stringify({ result }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

Workers ortamında cold start’ları minimize etmek için modülü global scope’ta tutmak çok önemli. Her request handler’da yeniden instantiate etmek hem yavaş hem de gereksiz.

Build Pipeline Otomasyonu

Tüm bu optimizasyonları bir araya getiren bir build script:

#!/bin/bash
set -e

TARGET="wasm32-unknown-unknown"
PACKAGE_NAME="my_wasm_module"
OUT_DIR="pkg"

echo "==> Rust ile release build..."
cargo build --target $TARGET --release

WASM_FILE="target/$TARGET/release/$PACKAGE_NAME.wasm"

echo "==> wasm-bindgen çalıştırılıyor..."
wasm-bindgen $WASM_FILE 
  --out-dir $OUT_DIR 
  --target web 
  --no-typescript

BINDGEN_WASM="$OUT_DIR/${PACKAGE_NAME}_bg.wasm"

echo "==> Boyut (ham): $(wc -c < $BINDGEN_WASM) bytes"

echo "==> wasm-opt optimizasyonu..."
wasm-opt -Oz 
  --strip-debug 
  --strip-producers 
  --zero-filled-memory 
  $BINDGEN_WASM -o $BINDGEN_WASM

echo "==> Boyut (optimize): $(wc -c < $BINDGEN_WASM) bytes"

echo "==> Brotli compression..."
brotli -q 11 -f $BINDGEN_WASM -o "${BINDGEN_WASM}.br"
gzip -9 -f -k $BINDGEN_WASM

echo "==> Boyut (brotli): $(wc -c < ${BINDGEN_WASM}.br) bytes"
echo "==> Boyut (gzip): $(wc -c < ${BINDGEN_WASM}.gz) bytes"

# Boyut kazancı hesaplama
ORIGINAL=$(stat -c%s "$WASM_FILE" 2>/dev/null || stat -f%z "$WASM_FILE")
FINAL=$(stat -c%s "${BINDGEN_WASM}.br" 2>/dev/null || stat -f%z "${BINDGEN_WASM}.br")
SAVINGS=$(( (ORIGINAL - FINAL) * 100 / ORIGINAL ))

echo "==> Toplam boyut küçültme: %$SAVINGS"
echo "==> Build tamamlandı."

Bu scripti CI/CD pipeline’ınıza ekleyin ve her deployment öncesi otomatik çalışsın. Ben GitLab CI’da bu scripti kullanıyorum ve her PR’da boyut değişimini artifact olarak kaydediyorum.

Performance Measurement: Ölçmeden Optimize Edemezsiniz

// WASM yükleme performansını ölçmek
async function measureWasmLoad(url) {
  const marks = {};
  
  performance.mark('wasm-fetch-start');
  const response = await fetch(url);
  performance.mark('wasm-fetch-end');
  
  performance.mark('wasm-compile-start');
  const module = await WebAssembly.compileStreaming(
    Promise.resolve(response)
  );
  performance.mark('wasm-compile-end');
  
  performance.mark('wasm-instantiate-start');
  const instance = await WebAssembly.instantiate(module, importObject);
  performance.mark('wasm-instantiate-end');
  
  const fetchTime = performance.measure(
    'wasm-fetch', 'wasm-fetch-start', 'wasm-fetch-end'
  ).duration;
  
  const compileTime = performance.measure(
    'wasm-compile', 'wasm-compile-start', 'wasm-compile-end'
  ).duration;
  
  const instantiateTime = performance.measure(
    'wasm-instantiate', 'wasm-instantiate-start', 'wasm-instantiate-end'
  ).duration;
  
  console.table({
    'Fetch süresi (ms)': fetchTime.toFixed(2),
    'Compile süresi (ms)': compileTime.toFixed(2),
    'Instantiate süresi (ms)': instantiateTime.toFixed(2),
    'Toplam (ms)': (fetchTime + compileTime + instantiateTime).toFixed(2)
  });
  
  return instance;
}

Bu ölçümleri production’da da toplayın, tercihen bir monitoring servisine gönderin. “Optimize etmek istiyorum” diyenlerin yarısı hangi metriği iyileştirmek istediğini bilmiyor. Önce ölçün.

Pratik Sonuçlar ve Öneriler

Gerçek bir projede uyguladığım bu tekniklerin kümülatif etkisini paylaşayım:

  • Başlangıç WASM boyutu: 4.2MB
  • opt-level = "z" + lto = true sonrası: 2.8MB
  • wasm-opt -Oz sonrası: 1.9MB
  • wee_alloc kullanımı sonrası: 1.6MB
  • Brotli compression sonrası: 380KB

Network üzerinden giden boyut 4.2MB’dan 380KB’a düştü. Yükleme süresi (3G bağlantıda) 14 saniyeden 2.3 saniyeye indi.

Bu rakamlar projeye göre değişir ama yöntem değişmez. Her optimizasyon katmanı bir öncekinin üstüne biniyor.

Sonuç

WASM optimizasyonu, build configuration’dan CDN konfigürasyonuna kadar uzanan çok katmanlı bir süreç. Tek bir sihirli çözüm yok ama doğru araçları doğru sırayla uyguladığınızda etkisi çarpıcı oluyor.

Önce cargo bloat ile neyin büyük yer kapladığını anlayın. Sonra build parametrelerini optimize edin, wasm-opt uygulayın, compression’ı aktive edin ve instantiateStreaming kullanın. Her adımda ölçün, kaydettiğiniz kazancı görmek hem motive edici hem de bir sonraki adımı planlamanıza yardımcı oluyor.

Edge computing bağlamında küçük modüller sadece kullanıcı deneyimi için değil, cold start süreleri ve işlem maliyetleri için de kritik. 4MB modül ile 380KB modül arasındaki fark, Cloudflare Workers gibi platformlarda direkt fatura farkına yansıyor.

Son bir not: bu optimizasyonları bir kere yapıp unutmayın. Bağımlılıklar güncellendikçe, yeni özellikler eklendikçe boyutlar değişiyor. Boyut monitöring’i CI/CD pipeline’ınızın kalıcı bir parçası olsun.

Bir yanıt yazın

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