WEBASSEMBLY MICRO-FRONTENDS FRONTEND-ARCHITECTURE WEB-PERFORMANCE JAVASCRIPT INTEROPERABILITY SYSTEM-DESIGN COMMUNICATION DATA-TRANSFER SHARED-MEMORY WEB-WORKERS COMPONENT-MODEL

Membangun Sistem Komunikasi Efisien Antar WebAssembly Micro-Frontends: Pola dan Praktik Terbaik

⏱️ 13 menit baca
👨‍💻

Membangun Sistem Komunikasi Efisien Antar WebAssembly Micro-Frontends: Pola dan Praktik Terbaik

1. Pendahuluan

Dunia pengembangan web terus berevolusi, dan dengan kompleksitas aplikasi yang semakin meningkat, arsitektur seperti Micro-Frontends (MF) menjadi pilihan populer. MF memungkinkan tim untuk mengembangkan, menguji, dan mendeploy bagian-bagian UI secara independen, meningkatkan skalabilitas dan agilitas. Namun, MF membawa tantangan baru, terutama dalam hal komunikasi antar bagian-bagian yang terpisah ini.

Di sisi lain, WebAssembly (Wasm) telah muncul sebagai revolusi di frontend, membawa performa mendekati native ke browser. Dengan Wasm, kita bisa menulis logika bisnis atau komponen UI yang performa-kritis menggunakan bahasa seperti Rust, C++, atau Go, dan menjalankannya di browser dengan kecepatan luar biasa.

Ketika WebAssembly bertemu dengan Micro-Frontends, kita mendapatkan kombinasi yang sangat kuat: Micro-Frontends yang tidak hanya modular tetapi juga berkinerja tinggi dan bisa ditulis dalam berbagai bahasa. Artikel “Mengintegrasikan WebAssembly sebagai Micro-Frontend” sudah membahas dasar-dasarnya. Namun, ada satu aspek krusial yang sering menjadi bottleneck di arsitektur terdistribusi: komunikasi antar MF.

Bagaimana Micro-Frontends yang ditulis dalam WebAssembly bisa berkomunikasi satu sama lain, atau dengan host JavaScript mereka, secara efisien? Ini bukan sekadar tentang mengirim data, tapi juga tentang menjaga performa, isolasi, dan fleksibilitas. Artikel ini akan menggali berbagai pola dan praktik terbaik untuk membangun sistem komunikasi yang efisien antar WebAssembly Micro-Frontends, memastikan aplikasi Anda tetap cepat, responsif, dan mudah dikelola.

2. Mengapa Komunikasi Efisien Penting dalam WebAssembly Micro-Frontends?

Bayangkan Micro-Frontends sebagai tim-tim kecil yang bekerja di sebuah proyek besar. Jika mereka kesulitan berkomunikasi atau membutuhkan waktu lama untuk bertukar informasi, proyek akan terhambat. Hal yang sama berlaku untuk Wasm Micro-Frontends.

📌 Masalah yang Dipecahkan oleh Komunikasi Efisien:

🎯 Tujuan kita: Membangun jembatan komunikasi yang kokoh dan cepat, seolah-olah semua bagian aplikasi adalah satu kesatuan yang kohesif, tanpa mengorbankan keuntungan modularitas dan performa WebAssembly.

3. Pola Komunikasi Dasar: JavaScript sebagai Jembatan Utama

Pada dasarnya, interaksi antara WebAssembly dan dunia luar selalu melewati JavaScript. JavaScript bertindak sebagai “mandor” yang mengatur komunikasi.

✅ 3.1. Memanggil Fungsi Wasm dari JavaScript (Exports)

Ini adalah cara paling umum Wasm berinteraksi dengan host JavaScript. Modul Wasm mengekspor fungsi-fungsi yang dapat dipanggil oleh JavaScript.

Use Case: Wasm MF bertanggung jawab untuk komputasi intensif (misal, pemrosesan gambar, algoritma kriptografi, atau logika bisnis kompleks) dan mengekspos fungsi untuk memulai komputasi tersebut.

// index.js (Host JavaScript)
async function init() {
  const { instance } = await WebAssembly.instantiateStreaming(
    fetch('my_wasm_mf.wasm'),
    {} // Tidak ada imports untuk contoh ini
  );

  // Memanggil fungsi yang diekspor oleh Wasm MF
  const result = instance.exports.add(5, 3);
  console.log(`Hasil dari Wasm MF: ${result}`); // Output: Hasil dari Wasm MF: 8

  // Jika Wasm MF memiliki fungsi untuk memproses data
  const processedDataPointer = instance.exports.process_data(someInputPointer);
  // ... kemudian JavaScript membaca data dari memori Wasm
}

init();
// my_wasm_mf.rs (Contoh Rust yang dikompilasi ke Wasm)
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Contoh lain: Wasm mengelola memori sendiri
// fn process_data(input_ptr: *const u8, len: usize) -> *mut u8 { /* ... */ }
// Atau Wasm mengembalikan pointer ke data di memori Wasm
// #[no_mangle]
// pub extern "C" fn allocate_buffer(size: usize) -> *mut u8 { /* ... */ }

Keterbatasan:

✅ 3.2. Memanggil Fungsi JavaScript dari Wasm (Imports)

Sebaliknya, modul Wasm juga bisa memanggil fungsi JavaScript yang disediakan kepadanya saat inisialisasi. Ini berguna untuk hal-hal seperti logging, mengakses DOM, atau API browser lainnya yang tidak bisa diakses langsung dari Wasm.

Use Case: Wasm MF perlu mencatat log ke konsol browser, atau memicu perubahan UI melalui JavaScript.

// index.js (Host JavaScript)
async function init() {
  const importObject = {
    env: {
      log_message: (ptr, len) => {
        // Asumsi Wasm menulis string ke memori
        const decoder = new TextDecoder('utf-8');
        const buffer = new Uint8Array(instance.exports.memory.buffer, ptr, len);
        const message = decoder.decode(buffer);
        console.log(`Pesan dari Wasm MF: ${message}`);
      }
    }
  };

  const { instance } = await WebAssembly.instantiateStreaming(
    fetch('my_wasm_mf.wasm'),
    importObject
  );

  instance.exports.greet();
}

init();
// my_wasm_mf.rs (Contoh Rust)
extern "C" {
    fn log_message(ptr: *const u8, len: usize);
}

#[no_mangle]
pub extern "C" fn greet() {
    let message = "Halo dari Wasm MF!";
    let ptr = message.as_ptr();
    let len = message.len();
    unsafe {
        log_message(ptr, len);
    }
}

Keterbatasan:

⚠️ Penting: Pola dasar ini sangat berguna untuk interaksi sederhana atau saat performa data transfer bukan prioritas utama. Namun, untuk aplikasi skala besar dengan banyak data atau interaksi sering, kita perlu solusi yang lebih canggih.

4. Komunikasi Asynchronous dan Event-Driven: Decoupling Micro-Frontends

Untuk skenario di mana Micro-Frontends perlu bereaksi terhadap perubahan state atau tindakan dari MF lain tanpa harus tahu detail implementasi satu sama lain, pendekatan event-driven adalah kuncinya.

✅ 4.1. Custom Events dan Event Bus

Ini adalah cara klasik untuk decoupling komponen UI. Wasm MF bisa memancarkan Custom Event melalui host JavaScript, dan MF lain bisa mendengarkannya.

Use Case: Wasm MF “Keranjang Belanja” menambahkan item, dan Wasm MF “Notifikasi” perlu menampilkan jumlah item baru.

// index.js (Host JavaScript / Event Bus)
const eventBus = new EventTarget();

// Fungsi helper untuk Wasm MF memancarkan event
window.dispatchWasmEvent = (eventNamePtr, eventNameLen, detailPtr, detailLen) => {
  const eventName = new TextDecoder().decode(
    new Uint8Array(wasmInstance.exports.memory.buffer, eventNamePtr, eventNameLen)
  );
  const detail = JSON.parse(new TextDecoder().decode(
    new Uint8Array(wasmInstance.exports.memory.buffer, detailPtr, detailLen)
  ));
  eventBus.dispatchEvent(new CustomEvent(eventName, { detail }));
};

// MF Notifikasi mendengarkan event
eventBus.addEventListener('itemAddedToCart', (e) => {
  console.log(`MF Notifikasi: Item ${e.detail.itemName} ditambahkan ke keranjang.`);
  // Update UI MF Notifikasi
});

// Inisialisasi Wasm MF Keranjang Belanja
async function initCartMF() {
  const importObject = {
    env: {
      dispatch_event: (name_ptr, name_len, detail_ptr, detail_len) => {
        window.dispatchWasmEvent(name_ptr, name_len, detail_ptr, detail_len);
      }
    }
  };
  const { instance } = await WebAssembly.instantiateStreaming(fetch('cart_mf.wasm'), importObject);
  window.wasmInstance = instance; // Simpan instance untuk akses global
}
initCartMF();

// Dari Wasm MF Keranjang Belanja:
// asumsi ada fungsi untuk menambahkan item, lalu memanggil dispatch_event
// contoh di Rust:
// #[no_mangle]
// pub extern "C" fn add_item_and_dispatch() {
//    let event_name = "itemAddedToCart";
//    let detail_json = r#"{"itemName": "Buku", "quantity": 1}"#;
//    unsafe {
//        dispatch_event(event_name.as_ptr(), event_name.len(), detail_json.as_ptr(), detail_json.len());
//    }
// }

Kelebihan:

Kekurangan:

✅ 4.2. MessageChannel API dan Broadcast Channel API

API ini memungkinkan komunikasi langsung antar konteks JavaScript yang berbeda (misal, antar Web Workers, antar iframe, atau antar tab browser). Jika Wasm MF Anda diisolasi dalam Web Worker atau iframe, API ini sangat berguna.

Use Case: Wasm MF di satu tab perlu sinkron dengan Wasm MF di tab lain (misal, login state).

// shared_channel.js (akan diimport oleh kedua MF)
const broadcastChannel = new BroadcastChannel('wasm-mf-channel');

// Di Wasm MF A (melalui host JS-nya)
async function initMFA() {
  // ... inisialisasi Wasm MF A ...
  broadcastChannel.postMessage({ type: 'userLogin', userId: 123 });
}

// Di Wasm MF B (melalui host JS-nya)
async function initMFB() {
  // ... inisialisasi Wasm MF B ...
  broadcastChannel.onmessage = (event) => {
    if (event.data.type === 'userLogin') {
      console.log(`MF B menerima event login untuk user: ${event.data.userId}`);
      // Perbarui state Wasm MF B
    }
  };
}

Kelebihan:

Kekurangan:

5. Komunikasi Berkinerja Tinggi: Shared Memory (SharedArrayBuffer)

Ketika Anda membutuhkan transfer data yang sangat cepat antara JavaScript dan WebAssembly, atau antar dua instance Wasm, tanpa overhead penyalinan, SharedArrayBuffer (SAB) adalah jawabannya.

💡 Analogi: Bayangkan Anda dan rekan kerja memiliki dua papan tulis terpisah (memori JS dan memori Wasm). Untuk berbagi informasi, Anda harus menyalin tulisan dari satu papan ke papan lain. Ini lambat. Dengan SharedArrayBuffer, Anda berdua berbagi satu papan tulis yang sama. Anda bisa menulis, dan rekan kerja Anda bisa langsung membaca tanpa menyalin.

⚠️ 5.1. Persyaratan Cross-Origin Isolation

Untuk alasan keamanan, SharedArrayBuffer hanya tersedia di lingkungan yang terisolasi secara cross-origin. Ini berarti Anda harus mengkonfigurasi server Anda dengan HTTP Headers berikut:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Tanpa ini, SharedArrayBuffer tidak akan tersedia, dan mencoba menggunakannya akan menghasilkan error. Pelajari lebih lanjut di artikel “Cross-Origin Isolation: Membuka Gerbang Performa Tinggi dan Fitur Canggih Web dengan Keamanan Maksimal”.

✅ 5.2. Cara Kerja SharedArrayBuffer

  1. JavaScript membuat SharedArrayBuffer dan membagikannya ke modul Wasm.
  2. Wasm dapat mengakses buffer ini sebagai bagian dari memorinya (biasanya melalui view Uint8Array atau sejenisnya).
  3. Kedua sisi (JS dan Wasm) dapat membaca dan menulis ke buffer yang sama secara bersamaan.
  4. Untuk mencegah race conditions (karena keduanya bisa menulis pada saat yang sama), Anda perlu menggunakan objek Atomics untuk sinkronisasi.

Use Case: Wasm MF melakukan pemrosesan video frame-by-frame, dan hasilnya perlu segera ditampilkan oleh JavaScript atau diumpankan ke MF Wasm lain untuk analisis.

// index.js (Host JavaScript)
async function init() {
  // Buat SharedArrayBuffer berukuran 1MB
  const sharedBuffer = new SharedArrayBuffer(1024 * 1024);
  const sharedArray = new Int32Array(sharedBuffer); // Untuk contoh Atomics

  const importObject = {
    env: {
      // Wasm akan membutuhkan akses ke buffer ini
      // Dalam kasus nyata, Anda bisa pass pointer ke buffer ke Wasm function
    }
  };

  const { instance } = await WebAssembly.instantiateStreaming(
    fetch('high_perf_mf.wasm'),
    importObject
  );

  // Wasm mengekspor fungsi yang mengambil SharedArrayBuffer
  // Ini adalah cara sederhana, bisa lebih kompleks dengan passing pointer
  instance.exports.set_shared_buffer(sharedBuffer);

  // Contoh penggunaan Atomics:
  // JS menulis nilai dan memberitahu Wasm
  Atomics.store(sharedArray, 0, 100);
  Atomics.notify(sharedArray, 0); // Memberitahu Wasm bahwa ada perubahan

  // JS menunggu Wasm selesai memproses
  Atomics.wait(sharedArray, 1, 0); // Menunggu Wasm menulis ke index 1
  console.log(`JS menerima hasil dari Wasm: ${sharedArray[1]}`);
}

init();
// high_perf_mf.rs (Contoh Rust)
static mut SHARED_BUFFER: Option<std::sync::Arc<[u8]>> = None; // Menggunakan Arc untuk SharedArrayBuffer

#[no_mangle]
pub extern "C" fn set_shared_buffer(ptr: *mut u8, len: usize) {
    unsafe {
        SHARED_BUFFER = Some(std::sync::Arc::from(std::slice::from_raw_parts(ptr, len)));
    }
}

#[no_mangle]
pub extern "C" fn process_data_in_shared_buffer() {
    unsafe {
        if let Some(buffer) = &SHARED_BUFFER {
            let shared_array = &*(buffer.as_ptr() as *const [i32; 2]); // Cast ke Int32Array
            // Contoh Atomics: Wasm menunggu JS menulis ke index 0
            // Ini akan menjadi panggilan ke JS Atomics.wait
            // Implementasi Atomics di Wasm biasanya melalui library atau JS imports
            // Untuk demo, kita asumsikan Wasm membaca data langsung
            // Contoh sederhana tanpa Atomics untuk ilustrasi:
            let value_from_js = shared_array[0];
            println!("Wasm menerima nilai dari JS: {}", value_from_js);

            // Wasm memproses dan menulis hasil ke index 1
            let result = value_from_js * 2;
            let shared_array_mut = &mut *(buffer.as_ptr() as *mut [i32; 2]);
            shared_array_mut[1] = result;
            // Memberitahu JS bahwa Wasm selesai
            // Ini akan menjadi panggilan ke JS Atomics.notify
        }
    }
}

Catatan: Implementasi Atomics di Wasm biasanya memerlukan js_sys::Atomics di Rust atau WebAssembly.Runtime.Atomics di Emscripten, dan bukan hanya std::sync::Arc. Contoh di atas disederhanakan untuk ilustrasi konsep.

Kelebihan:

Kekurangan:

6. Memanfaatkan Web Workers untuk Isolasi dan Offloading

Untuk menjaga Main Thread tetap responsif, terutama saat Wasm MF melakukan komputasi yang berat, menempatkan Wasm MF di dalam Web Worker adalah strategi yang sangat efektif.

✅ 6.1. Wasm di Web Worker

Web Worker berjalan di thread terpisah, sehingga komputasi yang terjadi di dalamnya tidak akan memblokir Main Thread (tempat UI berada). Komunikasi antara Main Thread dan Web Worker dilakukan melalui postMessage dan onmessage.

Use Case: Wasm MF melakukan simulasi fisika kompleks atau pemrosesan data besar yang membutuhkan waktu, tanpa mengganggu interaksi pengguna di UI.

// worker.js
self.onmessage = async (e) => {
  if (e.data.type === 'init_wasm') {
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch('heavy_compute_mf.wasm'),
      {}
    );
    self.wasmInstance = instance;
    self.postMessage({ type: 'wasm_ready' });
  } else if (e.data.type === 'start_computation') {
    const input = e.data.payload;
    const result = self.wasmInstance.exports.perform_heavy_computation(input);
    self.postMessage({ type: 'computation_done', payload: result });
  }
};
// index.js (Main Thread)
const worker = new Worker('worker.js');

worker.onmessage =