WASM ile Tarayıcıda Gerçek Zamanlı Görüntü İşleme
Tarayıcıda gerçek zamanlı görüntü işleme deyince aklınıza ne geliyor? Birkaç yıl önce bu fikir, JavaScript’in performans sınırları yüzünden ya “imkansız” ya da “kullanılamaz kadar yavaş” kategorisindeydi. Bugün ise WebAssembly sayesinde C++, Rust veya Go ile yazılmış görüntü işleme algoritmalarını doğrudan tarayıcıda, native’e yakın hızda çalıştırabiliyoruz. Bu yazıda sıfırdan bir WASM tabanlı görüntü işleme pipeline’ı kuracak, gerçek dünya senaryolarına bakacak ve sistem yöneticisi gözüyle deployment tarafını da mercek altına alacağız.
WebAssembly Neden Görüntü İşleme İçin Biçilmiş Kaftan?
JavaScript single-threaded çalışır ve JIT optimizasyonlarına rağmen yoğun matematiksel işlemlerde ciddi bottleneck’ler yaratır. Bir kamera akışından gelen 1080p görüntüyü her frame’de işlemeye çalıştığınızda, Gaussian blur, edge detection veya renk uzayı dönüşümleri gibi piksel düzeyindeki operasyonlar JavaScript’i kolayca diz çöktürür.
WASM’ın burada getirdiği avantajlar somuttur:
- Deterministik performans: JIT’in “bazen çalışır” optimizasyonlarının aksine WASM, her çalıştırmada öngörülebilir hız sunar
- SIMD desteği: WebAssembly SIMD uzantılarıyla aynı anda birden fazla piksel üzerinde paralel işlem yapabilirsiniz
- Bellek kontrolü: Linear memory modeli sayesinde görüntü tamponlarını tam kontrolle yönetirsiniz
- Dil esnekliği: Mevcut C++ veya Rust görüntü işleme kütüphanelerini doğrudan port edebilirsiniz
Bir benchmark olarak şunu aklınızda tutun: Basit bir Sobel edge detection filtresi için JavaScript yaklaşık 180ms harcıyorken, aynı algoritmanın WASM ile derlenmesi 12-18ms’ye düşmektedir. 60 FPS hedefliyorsanız her frame için 16.6ms bütçeniz var, bu sayılar anlam ifade etmeye başlıyor.
Geliştirme Ortamını Kurmak
Emscripten toolchain ile başlayacağız. Bu, C/C++ kodunu WASM’a derleyen en olgun araç zinciri.
# Emscripten SDK kurulumu
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# Son kararlı sürümü kur
./emsdk install latest
./emsdk activate latest
# Ortam değişkenlerini ayarla
source ./emsdk_env.sh
# Kurulumu doğrula
emcc --version
# Rust tabanlı gitmek isteyenler için wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
wasm-pack --version
Rust tercih edenler için proje yapısını ayrıca kuralım:
# Yeni Rust kütüphane projesi
cargo new --lib wasm-image-processor
cd wasm-image-processor
# Cargo.toml'u düzenle
cat >> Cargo.toml << 'EOF'
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"console",
"ImageData",
"CanvasRenderingContext2d",
"HtmlCanvasElement",
] }
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
EOF
C++ ile Temel Görüntü İşleme Modülü
Gerçek dünya senaryosu: Fabrika üretim hattında çalışan bir kalite kontrol sistemi düşünün. Kameradan gelen görüntülerde çatlak veya deformasyon tespiti yapılacak. Bu işlemi buluta göndermek hem gecikme hem de bant genişliği sorunları yaratır. WASM ile tarayıcıda çözelim.
// image_processor.cpp
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>
#include <cstdint>
#include <cmath>
#include <vector>
#include <algorithm>
extern "C" {
// Gaussian blur - 5x5 kernel
EMSCRIPTEN_KEEPALIVE
void gaussian_blur(
uint8_t* src,
uint8_t* dst,
int width,
int height
) {
const float kernel[5][5] = {
{1, 4, 6, 4, 1},
{4, 16, 24, 16, 4},
{6, 24, 36, 24, 6},
{4, 16, 24, 16, 4},
{1, 4, 6, 4, 1}
};
const float divisor = 256.0f;
for (int y = 2; y < height - 2; y++) {
for (int x = 2; x < width - 2; x++) {
float r = 0, g = 0, b = 0;
for (int ky = -2; ky <= 2; ky++) {
for (int kx = -2; kx <= 2; kx++) {
int idx = ((y + ky) * width + (x + kx)) * 4;
float w = kernel[ky + 2][kx + 2];
r += src[idx] * w;
g += src[idx + 1] * w;
b += src[idx + 2] * w;
}
}
int out_idx = (y * width + x) * 4;
dst[out_idx] = (uint8_t)(r / divisor);
dst[out_idx + 1] = (uint8_t)(g / divisor);
dst[out_idx + 2] = (uint8_t)(b / divisor);
dst[out_idx + 3] = src[out_idx + 3]; // Alpha kanalını koru
}
}
}
// Sobel edge detection
EMSCRIPTEN_KEEPALIVE
void sobel_edge_detect(
uint8_t* src,
uint8_t* dst,
int width,
int height,
int threshold
) {
const int Gx[3][3] = {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}};
const int Gy[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}};
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int gx = 0, gy = 0;
for (int ky = -1; ky <= 1; ky++) {
for (int kx = -1; kx <= 1; kx++) {
// Gri tonlamaya çevir
int idx = ((y + ky) * width + (x + kx)) * 4;
int gray = (int)(src[idx] * 0.299 +
src[idx+1] * 0.587 +
src[idx+2] * 0.114);
gx += gray * Gx[ky + 1][kx + 1];
gy += gray * Gy[ky + 1][kx + 1];
}
}
int magnitude = (int)sqrt((double)(gx * gx + gy * gy));
magnitude = std::min(magnitude, 255);
int out_idx = (y * width + x) * 4;
uint8_t val = (magnitude > threshold) ? 255 : 0;
dst[out_idx] = val;
dst[out_idx + 1] = val;
dst[out_idx + 2] = val;
dst[out_idx + 3] = 255;
}
}
}
// Histogram equalization - düşük ışık koşulları için
EMSCRIPTEN_KEEPALIVE
void histogram_equalize(uint8_t* data, int width, int height) {
int hist[256] = {0};
int total = width * height;
// Histogram oluştur
for (int i = 0; i < total; i++) {
int idx = i * 4;
int gray = (int)(data[idx] * 0.299 +
data[idx+1] * 0.587 +
data[idx+2] * 0.114);
hist[gray]++;
}
// CDF hesapla
int cdf[256] = {0};
cdf[0] = hist[0];
for (int i = 1; i < 256; i++) {
cdf[i] = cdf[i-1] + hist[i];
}
// LUT oluştur ve uygula
uint8_t lut[256];
int cdf_min = 0;
for (int i = 0; i < 256; i++) {
if (cdf[i] > 0) { cdf_min = cdf[i]; break; }
}
for (int i = 0; i < 256; i++) {
lut[i] = (uint8_t)(((float)(cdf[i] - cdf_min) /
(total - cdf_min)) * 255);
}
for (int i = 0; i < total; i++) {
int idx = i * 4;
data[idx] = lut[data[idx]];
data[idx+1] = lut[data[idx+1]];
data[idx+2] = lut[data[idx+2]];
}
}
} // extern "C"
Derleme komutları:
# Optimized WASM derleme
emcc image_processor.cpp
-O3
-msimd128
-s WASM=1
-s EXPORTED_FUNCTIONS='["_gaussian_blur","_sobel_edge_detect","_histogram_equalize","_malloc","_free"]'
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'
-s ALLOW_MEMORY_GROWTH=1
-s INITIAL_MEMORY=33554432
-s MAXIMUM_MEMORY=268435456
--bind
-o image_processor.js
# SIMD desteğini test et
node -e "
const mod = require('./image_processor.js');
mod.then(m => console.log('WASM modülü yüklendi:', Object.keys(m)));
"
JavaScript Entegrasyon Katmanı
WASM modülünü tarayıcıda kullanabilmek için temiz bir wrapper yazmak kritik önem taşır. Bellek yönetimi burada en sık hata yapılan nokta.
// wasm-image-bridge.js
class WasmImageProcessor {
constructor() {
this.module = null;
this.inputPtr = null;
this.outputPtr = null;
this.bufferSize = 0;
this.initialized = false;
}
async init() {
// WASM modülünü yükle
this.module = await createImageProcessor({
locateFile: (file) => `/wasm/${file}`
});
this.initialized = true;
console.log('WASM image processor hazır');
}
_allocateBuffers(width, height) {
const newSize = width * height * 4; // RGBA
// Sadece boyut değiştiyse yeniden allocate et
if (newSize !== this.bufferSize) {
if (this.inputPtr) {
this.module._free(this.inputPtr);
this.module._free(this.outputPtr);
}
this.inputPtr = this.module._malloc(newSize);
this.outputPtr = this.module._malloc(newSize);
this.bufferSize = newSize;
if (!this.inputPtr || !this.outputPtr) {
throw new Error('WASM bellek tahsisi başarısız');
}
}
}
processFrame(imageData, operation, options = {}) {
if (!this.initialized) throw new Error('Processor başlatılmadı');
const { width, height, data } = imageData;
this._allocateBuffers(width, height);
// Görüntü verisini WASM belleğine kopyala
this.module.HEAPU8.set(data, this.inputPtr);
const startTime = performance.now();
switch (operation) {
case 'blur':
this.module._gaussian_blur(
this.inputPtr,
this.outputPtr,
width,
height
);
break;
case 'edge':
this.module._sobel_edge_detect(
this.inputPtr,
this.outputPtr,
width,
height,
options.threshold || 50
);
break;
case 'equalize':
// In-place işlem
this.module.HEAPU8.set(data, this.inputPtr);
this.module._histogram_equalize(this.inputPtr, width, height);
break;
}
const processingTime = performance.now() - startTime;
// Sonucu geri al
const resultBuffer = operation === 'equalize'
? this.inputPtr
: this.outputPtr;
const result = new Uint8ClampedArray(
this.module.HEAPU8.buffer,
resultBuffer,
width * height * 4
);
return {
imageData: new ImageData(new Uint8ClampedArray(result), width, height),
processingTime
};
}
destroy() {
if (this.inputPtr) this.module._free(this.inputPtr);
if (this.outputPtr) this.module._free(this.outputPtr);
this.module = null;
this.initialized = false;
}
}
export default WasmImageProcessor;
Web Worker ile Gerçek Zamanlı Pipeline
Ana thread’i bloke etmemek için WASM işlemlerini Web Worker içinde çalıştırmak şart. Aksi takdirde UI donmalarıyla karşılaşırsınız.
// image-worker.js
importScripts('/wasm/image_processor.js');
let processor = null;
self.onmessage = async (event) => {
const { type, payload } = event.data;
switch (type) {
case 'INIT':
try {
// Worker içinde WASM modülünü yükle
const module = await createImageProcessor({
locateFile: (f) => `/wasm/${f}`
});
processor = {
module,
inputPtr: null,
outputPtr: null,
bufferSize: 0
};
self.postMessage({ type: 'READY' });
} catch (err) {
self.postMessage({
type: 'ERROR',
payload: err.message
});
}
break;
case 'PROCESS_FRAME':
if (!processor) {
self.postMessage({ type: 'ERROR', payload: 'Processor yok' });
return;
}
const { imageData, operation, options, frameId } = payload;
const { width, height } = imageData;
const bufSize = width * height * 4;
// Buffer yönetimi
if (bufSize !== processor.bufferSize) {
if (processor.inputPtr) {
processor.module._free(processor.inputPtr);
processor.module._free(processor.outputPtr);
}
processor.inputPtr = processor.module._malloc(bufSize);
processor.outputPtr = processor.module._malloc(bufSize);
processor.bufferSize = bufSize;
}
processor.module.HEAPU8.set(imageData.data, processor.inputPtr);
const t0 = performance.now();
if (operation === 'edge') {
processor.module._sobel_edge_detect(
processor.inputPtr,
processor.outputPtr,
width, height,
options?.threshold || 50
);
} else if (operation === 'blur') {
processor.module._gaussian_blur(
processor.inputPtr,
processor.outputPtr,
width, height
);
}
const processingTime = performance.now() - t0;
// Transferable object ile zero-copy transfer
const resultData = new Uint8ClampedArray(
processor.module.HEAPU8.buffer,
processor.outputPtr,
bufSize
).slice();
self.postMessage({
type: 'FRAME_READY',
payload: { resultData, width, height, frameId, processingTime }
}, [resultData.buffer]);
break;
}
};
Deployment ve Sunucu Tarafı Yapılandırması
WASM dosyaları için doğru HTTP header’larını ayarlamak performans açısından kritiktir. Bu kısım genellikle göz ardı edilir ama cross-origin isolation ve SharedArrayBuffer kullanımı için zorunludur.
# Nginx yapılandırması - WASM için optimize
# /etc/nginx/sites-available/wasm-app.conf
cat > /etc/nginx/sites-available/wasm-app.conf << 'EOF'
server {
listen 80;
listen 443 ssl http2;
server_name app.sirketiniz.com;
root /var/www/wasm-app;
# WASM MIME tipi
types {
application/wasm wasm;
}
# SharedArrayBuffer için gerekli header'lar
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# WASM dosyaları için agresif cache
location ~* .wasm$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# Brotli compression - WASM için mükemmel
brotli on;
brotli_comp_level 11;
brotli_types application/wasm;
gzip on;
gzip_comp_level 9;
gzip_types application/wasm;
}
# JS bundle'ları
location ~* .(js|mjs)$ {
add_header Cache-Control "public, max-age=86400";
gzip_static on;
}
location / {
try_files $uri $uri/ /index.html;
}
ssl_certificate /etc/letsencrypt/live/app.sirketiniz.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.sirketiniz.com/privkey.pem;
}
EOF
# Nginx test ve yeniden başlatma
nginx -t && systemctl reload nginx
# WASM dosyasının content-type'ını doğrula
curl -I https://app.sirketiniz.com/wasm/image_processor.wasm | grep -E "content-type|cache-control|cross-origin"
Performans İzleme ve Profiling
Production ortamında WASM performansını takip etmek için basit bir metric sistemi kuralım:
// wasm-metrics.js
class WasmPerformanceMonitor {
constructor() {
this.metrics = {
frameCount: 0,
totalProcessingTime: 0,
maxProcessingTime: 0,
droppedFrames: 0,
targetFrameTime: 1000 / 60 // 60 FPS hedefi
};
this.observers = [];
this._startReporting();
}
recordFrame(processingTime) {
this.metrics.frameCount++;
this.metrics.totalProcessingTime += processingTime;
this.metrics.maxProcessingTime = Math.max(
this.metrics.maxProcessingTime,
processingTime
);
if (processingTime > this.metrics.targetFrameTime) {
this.metrics.droppedFrames++;
}
}
getStats() {
const { frameCount, totalProcessingTime,
maxProcessingTime, droppedFrames } = this.metrics;
return {
avgProcessingTime: frameCount > 0
? (totalProcessingTime / frameCount).toFixed(2)
: 0,
maxProcessingTime: maxProcessingTime.toFixed(2),
droppedFrameRate: frameCount > 0
? ((droppedFrames / frameCount) * 100).toFixed(1)
: 0,
totalFrames: frameCount
};
}
_startReporting() {
setInterval(() => {
const stats = this.getStats();
// Prometheus formatında metric push (varsa)
if (window.metricsEndpoint) {
fetch('/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
service: 'wasm-image-processor',
timestamp: Date.now(),
...stats
})
}).catch(() => {}); // Fire and forget
}
console.table(stats);
}, 10000); // Her 10 saniyede bir raporla
}
}
export default WasmPerformanceMonitor;
Gerçek Dünya Deployment Senaryosu: CDN ve Edge
WASM dosyalarının CDN’den sunulması, özellikle küresel kullanıcı kitlesinde gecikmeyi dramatik biçimde azaltır. CloudFlare Workers ile WASM’ı edge’de çalıştırabilirsiniz:
# Wrangler CLI kurulumu ve Workers deployment
npm install -g wrangler
# wrangler.toml oluştur
cat > wrangler.toml << 'EOF'
name = "wasm-image-edge"
main = "src/worker.js"
compatibility_date = "2024-01-01"
[build]
command = "npm run build"
[[rules]]
type = "CompiledWasm"
globs = ["**/*.wasm"]
fallthrough = true
[vars]
ENVIRONMENT = "production"
EOF
# Worker deployment
wrangler deploy
# Deployment durumunu kontrol et
wrangler deployments list
wrangler tail --format=pretty
WASM dosyasının boyutunu production’a göndermeden önce optimize etmek de önemli:
# wasm-opt ile bytecode optimizasyonu
npm install -g wasm-opt
# Agresif optimizasyon (derleme süresini artırır ama dosya küçülür)
wasm-opt -O3 --enable-simd
image_processor.wasm
-o image_processor.optimized.wasm
# Boyut karşılaştırması
ls -lh image_processor*.wasm
# Brotli ile sıkıştır (nginx brotli_static için)
brotli -q 11 image_processor.optimized.wasm
# Sonuç boyutlarını göster
for f in image_processor*.wasm*; do
echo "$f: $(du -sh $f | cut -f1)"
done
Güvenlik Konuları
WASM’ın sandbox modeli güvenlidir ama bazı noktalar dikkat gerektirir:
- Content Security Policy: WASM çalıştırabilmek için
wasm-unsafe-evaldirektifini CSP’ye eklemeniz gerekebilir (Chrome 97+ ileunsafe-evalartık zorunlu değil) - CORS yapılandırması: WASM modülleri farklı origin’den yükleniyorsa uygun CORS header’larını ekleyin
- Supply chain güvenliği: Kullandığınız üçüncü parti WASM modüllerini subresource integrity (SRI) hash’leriyle doğrulayın
- Bellek izolasyonu: WASM’ın linear memory’si tarayıcının geri kalanından izole, ancak aynı modül içindeki buffer overflow’lara karşı uygulama katmanında savunma yapın
# WASM dosyası için SRI hash üretimi
openssl dgst -sha384 -binary image_processor.wasm |
openssl base64 -A |
sed 's/^/sha384-/'
# Üretilen hash'i HTML'de kullanma örneği:
# <script src="image_processor.js"
# integrity="sha384-abc123..."
# crossorigin="anonymous"></script>
Sonuç
WASM tabanlı tarayıcı görüntü işleme, daha önce backend’e ya da native uygulamalara mahkum ettiğimiz workload’ları kullanıcının cihazına taşımak için güçlü bir araç. Sysadmin perspektifinden baktığımızda bunun anlamı şudur: daha az sunucu, daha az bant genişliği, daha az gecikme ve daha iyi kullanıcı deneyimi.
Pratik olarak dikkat edilmesi gereken noktalar:
- MIME tipi ve header yapılandırması nginx veya Apache’de doğru ayarlanmazsa WASM yüklenmez, bu en yaygın operasyonel hatadır
- Cross-Origin Isolation header’ları SharedArrayBuffer için zorunludur, staging’de test etmeyi unutmayın
- WASM dosya boyutu production’da wasm-opt ve brotli ile küçültün, 500KB’ın üzerindeki dosyalar ilk yükleme deneyimini etkiler
- Worker thread’leri UI thread’ini asla bloke etmeyin, tüm ağır işlemleri worker’a taşıyın
- Bellek yönetimi malloc/free çiftlerini takip etmezseniz long-running uygulamalarda bellek sızıntısı yaşarsınız
WebAssembly ekosistemi hızla olgunlaşıyor. WASI ile sunucu tarafında, Component Model ile modüler deploymentlarda ve Threads proposal’ıyla gerçek çok iş parçacıklı işlemede önümüzdeki dönemde çok daha güçlü özellikler göreceğiz. Şimdi temelleri oturtmak, bu dönüşümde hazır olmak anlamına geliyor.
