L
Vue.js β€’ Literally Web ID

Membangun Micro-Frontend Modular Skala Enterprise Menggunakan Vue 3 Composition API dan Vite Module Federation

⏱️ 18 menit baca β€’ πŸ“… 14 Oct 2026 β€’ πŸ‘οΈ 1 views
Membangun Micro-Frontend Modular Skala Enterprise Menggunakan Vue 3 Composition API dan Vite Module Federation

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan perangkat lunak enterprise modern, arsitektur monolitik frontend sering kali menjadi hambatan utama (bottleneck) bagi skalabilitas organisasi. Ketika sebuah perusahaan berkembangβ€”memiliki puluhan tim lintas fungsi (cross-functional squads) yang bekerja pada repositori kode tunggalβ€”masalah Git conflicts, waktu build yang membengkak, dan risiko regresi yang tinggi menjadi hal yang lumrah. Sebuah perubahan kecil pada modul billing oleh Tim A dapat merusak modul dashboard milik Tim B karena penggunaan state global yang tercemar atau dependensi paket npm yang tidak sinkron.

Secara historis, solusi untuk masalah ini adalah memecah frontend menjadi aplikasi-aplikasi terpisah yang kemudian digabungkan melalui iframe atau teknik server-side includes. Namun, pendekatan ini mendatangkan masalah baru: pengalaman pengguna yang terputus-putus, hilangnya state kontekstual saat navigasi, serta masalah performa akibat memuat ulang aset yang sama berkali-kali.

Di sisi lain, industri infrastruktur web telah bergeser menuju pendekatan decoupled dengan memanfaatkan edge caching, CDN (seperti Cloudflare), dan reverse proxy berbasis Nginx atau Kubernetes Ingress. Masalah utamanya bukan lagi bagaimana menyajikan file statis, melainkan bagaimana melakukan orkestrasi runtime antar-aplikasi mandiri (micro-frontends) secara efisien tanpa mengorbankan performa jaringan, keamanan CSP (Content Security Policy), dan konsistensi desain UI.

[!WARNING] Memaksa adopsi Micro-Frontend tanpa memetakan batasan domain bisnis yang jelas (Bounded Contexts) hanya akan memindahkan kerumitan monolit kode ke monolit jaringan (distributed monolith), yang jauh lebih sulit didebug dan diamankan.

Perbandingan Kondisi: Sebelum vs Sesudah

Metrik / Aspek Monolit Frontend Tradisional Micro-Frontend Modular (Vue 3 + Vite Module Federation)
Waktu Build (CI/CD) 15 - 45 Menit (Semua modul ikut di-build) 2 - 4 Menit (Build per remote secara independen)
Skalabilitas Tim Tinggi konflik Git, rilis harus bersamaan (monolithic release) Otonomi penuh per tim, rilis independen kapan saja
Ukuran Bundle Awal Monolitik raksasa (> 5MB parsed JS) Terdistribusi, hanya memuat modul yang dibutuhkan
Manajemen Dependensi Sering terjadi konflik versi paket npm global Shared dependencies di-load sekali via runtime container

2. Konsep Inti & Deep Dive Arsitektur

Arsitektur yang dibahas dalam panduan ini mengandalkan Vite Module Federation (melalui plugin @originjs/vite-plugin-federation atau native federation) yang dikombinasikan dengan Vue 3 Composition API dan Pinia untuk manajemen state terdistribusi.

Secara under the hood, Module Federation memungkinkan sebuah aplikasi JavaScript (disebut Host) untuk memuat kode secara dinamis dari aplikasi lain (disebut Remote) pada waktu runtime di browser. Berbeda dengan teknik code-splitting biasa yang dilakukan pada saat build-time, modul jarak jauh dapat dideploy ke server yang berbeda, S3 bucket terpisah, atau edge CDN yang berbeda pula.

Alur Eksekusi & Data Flow

+-------------------------------------------------------------+
|                        Browser Client                       | 
+-------------------------------------------------------------+
                               β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                                               β–Ό
+-----------------------+                   +-----------------------+
|    Host Container     | ──(Remote Entry)─>|    Remote Container   | 
| (Shell / Layout App)  |                   | (Domain: Billing Module)|
+-----------------------+                   +-----------------------+
       β”‚                                               β”‚
       β”œβ”€β”€ Pinia Shared Store                          β”œβ”€β”€ Vue 3 SFCs
       └── Vue Router                                  └── Composition API
  1. Inisialisasi Host: Browser mengakses aplikasi Host. Host mengunduh file remoteEntry.js dari server Remote berdasarkan konfigurasi manifes.
  2. Resolusi Dependensi: Host dan Remote melakukan dependency sharing (misalnya Vue 3, Pinia, Vue Router). Jika Host sudah memuat Vue 3, Remote tidak akan mengunduh Vue 3 lagi, melainkan menggunakan instance yang sama dari memori browser (singleton enforcement).
  3. Mounting Komponen: Host merender komponen remote secara lazy load menggunakan defineAsyncComponent Vue 3, menyisipkan DOM dan mengeksekusi logika Composition API dalam konteks eksekusi Host.

3. Perbandingan Komparatif / Analisis Pendekatan

Untuk memilih arsitektur frontend enterprise yang tepat, berikut adalah analisis mendalam antara tiga pendekatan utama yang sering dipertimbangkan oleh para arsitek sistem:

Parameter Arsitektural 1. Single-SPA / qiankun 2. Iframe-based Composition 3. Vite Module Federation (Pilihan Utama)
Isolation Level Rendah (Shared window scope, butuh sandbox CSS) Sempurna (Native iframe sandbox) Menengah (Shared JavaScript runtime, DOM terisolasi via Vue scope)
Bundle Sharing Kompleks (systemjs import maps) Tidak ada (Setiap iframe memuat ulang dependensi) Sangat Efisien (Shared dependencies via Webpack/Vite runtime)
Routing & History Kompleks, sering konflik event popstate Sangat sulit disinkronkan dengan URL parent Alami, terintegrasi mulus dengan Vue Router
Overhead CPU/RAM Rendah-Menengah Tinggi (Multiple DOM & JS context instances) Sangat Rendah (Single JS execution context)
Kompleksitas Setup Tinggi (Kurva belajar curam) Rendah Menengah (Membutuhkan pemahaman plugin bundler)

[!TIP] Gunakan Vite Module Federation jika tim Anda menggunakan tumpukan teknologi modern yang sama (misal Vue 3 di semua squad). Jika squad Anda menggunakan teknologi campuran (React, Vue, Angular secara bersamaan), pertimbangkan pendekatan Single-SPA dengan Web Component wrapper.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum menyelami kode implementasi, pastikan lingkungan pengembangan dan produksi Anda memenuhi standar enterprise berikut:

  • Node.js: Versi 18.x LTS atau 20.x LTS (disarankan menggunakan nvm untuk manajemen versi).
  • Package Manager: pnpm (direkomendasikan untuk efisiensi disk space monorepo/polyrepo dan manajemen symlink yang ketat) atau npm >= 10.
  • Web Server / Ingress: Nginx (untuk reverse proxy lokal/staging) atau Kubernetes Ingress Controller (Nginx Ingress / Traefik) untuk produksi.
  • Storage: AWS S3 / Minio untuk menyimpan aset build static remote.

Verifikasi instalasi dasar melalui terminal:

node -v && pnpm -v
# Output yang diharapkan: v20.x.x dan v9.x.x

5. Panduan Implementasi Langkah demi Langkah

Kita akan membangun dua aplikasi terpisah: Host Application (Port 3000) dan Billing Remote Application (Port 3001).

A. Setup Host Application (host-app)

Inisialisasi proyek Vite dengan Vue dan TypeScript:

pnpm create vite host-app --template vue-ts
cd host-app
pnpm install
pnpm add @originjs/vite-plugin-federation pinia vue-router@4

Buat file konfigurasi vite.config.ts pada root host-app:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'host-app',
      remotes: {
        billingModule: 'http://localhost:3001/assets/remoteEntry.js',
      },
      shared: ['vue', 'pinia', 'vue-router'],
    }),
  ],
  server: {
    port: 3000,
    cors: true,
  },
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

Buat file src/App.vue pada Host:

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
import { useUserStore } from './stores/userStore';

// Load remote component secara asinkron
const RemoteInvoices = defineAsyncComponent(() => 
  import('billingModule/InvoicesList')
);

const userStore = useUserStore();
userStore.setUser({ id: 'USR-9988', name: 'Enterprise Admin', role: 'SuperAdmin' });

const activeTab = ref<'home' | 'billing'>('home');
</script>

<template>
  <div class="host-container">
    <header class="host-header">
      <h1>Enterprise Shell (Host Application)</h1>
      <nav>
        <button @click="activeTab = 'home'">Home Dashboard</button>
        <button @click="activeTab = 'billing'">Billing & Invoices (Remote)</button>
      </nav>
    </header>

    <main class="host-content">
      <div v-if="activeTab === 'home'">
        <h2>Welcome, {{ userStore.userName }}</h2>
        <p>This is the core container running on port 3000.</p>
      </div>
      <div v-else>
        <Suspense>
          <template #default>
            <RemoteInvoices />
          </template>
          <template #fallback>
            <div class="loader">Loading Remote Billing Module...</div>
          </template>
        </Suspense>
      </div>
    </main>
  </div>
</template>

<style>
body {
  font-family: 'Inter', sans-serif;
  margin: 0;
  background-color: #0f172a;
  color: #f8fafc;
}
.host-container { padding: 2rem; }
.host-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #334155; padding-bottom: 1rem; }
.host-header nav button { margin-left: 1rem; padding: 0.5rem 1rem; background: #3b82f6; border: none; color: white; border-radius: 4px; cursor: pointer; }
.host-content { margin-top: 2rem; }
.loader { color: #38bdf8; font-style: italic; }
</style>

Buat store Pinia shared pada Host (src/stores/userStore.ts):

import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useUserStore = defineStore('user', () => {
  const userId = ref<string>('');
  const userName = ref<string>('');
  const userRole = ref<string>('');

  function setUser(payload: { id: string; name: string; role: string }) {
    userId.value = payload.id;
    userName.value = payload.name;
    userRole.value = payload.role;
  }

  return { userId, userName, userRole, setUser };
});

B. Setup Remote Application (billing-app)

Inisialisasi proyek Remote:

pnpm create vite billing-app --template vue-ts
cd billing-app
pnpm install
pnpm add @originjs/vite-plugin-federation pinia vue-router@4

Buat file konfigurasi vite.config.ts pada billing-app:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'billingModule',
      filename: 'remoteEntry.js',
      exposes: {
        './InvoicesList': './src/components/InvoicesList.vue',
      },
      shared: ['vue', 'pinia', 'vue-router'],
    }),
  ],
  server: {
    port: 3001,
    cors: true,
  },
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

Buat komponen Remote (src/components/InvoicesList.vue):

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useUserStore } from 'host-app/stores/userStore'; // Mengakses store yang dishare oleh Host

interface Invoice {
  id: string;
  amount: number;
  status: string;
}

const invoices = ref<Invoice[]>([]);
const loading = ref(true);
const userStore = useUserStore();

onMounted(() => {
  // Simulasi fetch data API enterprise
  setTimeout(() => {
    invoices.value = [
      { id: 'INV-2023-001', amount: 1500000, status: 'Paid' },
      { id: 'INV-2023-002', amount: 3500000, status: 'Pending' },
      { id: 'INV-2023-003', amount: 750000, status: 'Failed' },
    ];
    loading.value = false;
  }, 800);
});
</script>

<template>
  <div class="billing-module-card">
    <h3>Billing & Invoices Module (Remote App - Port 3001)</h3>
    <div class="user-context-banner">
      <p>Aktif sebagai: <strong>{{ userStore.userName }}</strong> (Role: {{ userStore.userRole }})</p>
    </div>

    <div v-if="loading" class="loading">Mengambil data finansial dari server...</div>
    
    <table v-else class="invoice-table">
      <thead>
        <tr>
          <th>Invoice ID</th>
          <th>Amount (IDR)</th>
          <th>Status</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="inv in invoices" :key="inv.id">
          <td>{{ inv.id }}</td>
          <td>Rp {{ inv.amount.toLocaleString() }}</td>
          <td>
            <span :class="['badge', inv.status.toLowerCase()]">{{ inv.status }}</span>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
.billing-module-card {
  background: #1e293b;
  padding: 1.5rem;
  border-radius: 8px;
  border: 1px solid #475569;
}
.user-context-banner {
  background: #0f172a;
  padding: 0.75rem;
  border-radius: 4px;
  margin-bottom: 1rem;
  border-left: 4px solid #38bdf8;
}
.invoice-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 1rem;
}
.invoice-table th, .invoice-table td {
  padding: 0.75rem;
  text-align: left;
  border-bottom: 1px solid #334155;
}
.badge {
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.85rem;
}
.badge.paid { background: #065f46; color: #34d399; }
.badge.pending { background: #78350f; color: #fbbf24; }
.badge.failed { background: #7f1d1d; color: #f87171; }
</style>

6. Bedah Kode & Penjelasan Detail Baris Kunci

  1. Shared Dependencies (shared: ['vue', 'pinia', 'vue-router']): Konfigurasi ini krusial. Jika vue tidak dijadikan shared, maka Host dan Remote akan membawa instance Vue mereka sendiri. Akibatnya, Composition API seperti provide/inject dan Pinia state management akan gagal karena adanya dua virtual DOM constructor yang berbeda di memori browser.

  2. defineAsyncComponent & Suspense: Di sisi Host, pemanggilan komponen remote dibungkus dengan defineAsyncComponent. Ini memberitahu Vue bahwa komponen tersebut harus diambil melalui jaringan secara on-demand. Tag <Suspense> memastikan bahwa fallback UI (loading spinner) dirender dengan mulus sebelum modul selesai diunduh dan dieksekusi.

  3. Cross-Application Pinia Store: Remote aplikasi dapat mengimpor store dari Host (import { useUserStore } from 'host-app/stores/userStore'). Ini memungkinkan state global seperti autentikasi user dibagikan tanpa perlu prop drilling atau event bus yang kompleks.


7. Skenario Produksi & Studi Kasus Dunia Nyata

Pada lingkungan enterprise skala besar dengan traffic tinggi (misal: 50.000 request per detik), arsitektur file statis untuk remote modules harus dikelola melalui CDN global (Cloudflare / AWS CloudFront) dengan strategi caching ketat.

Konfigurasi Nginx untuk Host & Remote

Untuk mencegah isu CORS (Cross-Origin Resource Sharing) dan memastikan remoteEntry.js tidak di-cache secara agresif saat ada rilis versi baru, berikut adalah konfigurasi Nginx production yang valid untuk server Remote:

server {
    listen 80;
    server_name billing.enterprise.internal;

    root /var/www/html/billing-app/dist;
    index index.html;

    # Security Headers Enterprise
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Konfigurasi khusus untuk remoteEntry.js agar tidak mengalami stale cache
    location = /assets/remoteEntry.js {
        add_header Access-Control-Allow-Origin "*";
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
        expires off;
    }

    # Asset statis lainnya dapat di-cache secara agresif
    location /assets/ {
        add_header Access-Control-Allow-Origin "*";
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

8. Common Pitfalls, Anti-Patterns & Solusi

  • Anti-Pattern 1: Menyimpan State Global Sensitif di LocalStorage secara Mentah

    • Dampak: Rentan terhadap serangan XSS di mana script pihak ketiga pada modul remote dapat mencuri token otorisasi.
    • Solusi: Gunakan HttpOnly Cookies untuk token autentikasi backend dan simpan state sesi ringkas di dalam memori Pinia store Host.
  • Pitfall 2: Versi Dependensi Shared yang Tidak Sinkron

    • Dampak: Runtime error TypeError: Cannot read properties of undefined saat komponen remote dirender.
    • Solusi: Terapkan validasi versi otomatis di CI/CD menggunakan monorepo tool (seperti Nx atau Turborepo) untuk memastikan versi vue dan pinia di Host dan seluruh Remote selalu identik secara persis (exact version match).

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk mengukur efisiensi sistem micro-frontend ini, lakukan pengujian beban menggunakan perangkat seperti k6 atau Lighthouse CI pada pipeline deployment.

Skrip Uji Beban k6 (load-test.js)

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 200 }, // Ramp-up ke 200 virtual users
    { duration: '1m', target: 500 },  // Stabil di 500 concurrent users
    { duration: '10s', target: 0 },   // Ramp-down
  ],
};

export default function () {
  const hostRes = http.get('https://app.enterprise.internal/');
  check(hostRes, { 'Host status is 200': (r) => r.status === 200 });

  const remoteEntryRes = http.get('https://billing.enterprise.internal/assets/remoteEntry.js');
  check(remoteEntryRes, { 'Remote Entry status is 200': (r) => r.status === 200 });

  sleep(1);
}

Jalankan benchmark:

k6 run load-test.js

[!IMPORTANT] Pastikan latensi jaringan antara server Host dan Remote berada di bawah 50ms. Gunakan lokasi region CDN yang berdekatan untuk menghindari Time to First Byte (TTFB) yang tinggi pada file remoteEntry.js.


10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan arsitektur Micro-Frontend berbasis Vue 3 dan Vite Module Federation ke lingkungan produksi enterprise, pastikan checklist berikut telah terpenuhi:

  • Validasi CORS: Header Access-Control-Allow-Origin terpasang dengan benar pada semua server remote.
  • Cache Bumping: File remoteEntry.js diatur dengan Cache-Control: no-store untuk mencegah stale code saat rollback.
  • Dependency Singleton Enforcement: Konfigurasi shared pada Vite plugin telah memetakan seluruh package inti Vue secara akurat.
  • Fallback Error Boundary: Komponen remote dibungkus dengan Error Boundary Vue (onErrorCaptured) untuk mencegah crash pada seluruh aplikasi Host jika modul remote gagal dimuat.
  • CSP Headers: Content Security Policy dikonfigurasi untuk mengizinkan eksekusi script dari domain remote yang sah.

Dengan menerapkan arsitektur ini secara disiplin, organisasi Anda dapat mencapai otonomi tim pengembang yang maksimal, siklus rilis yang cepat, serta performa aplikasi web enterprise yang tetap terjaga pada skala jutaan pengguna.

🏷️ Tags: #Vite #Micro-Frontend #Module Federation #Enterprise Architecture #Vue 3