Scaling Konten MDX di Next.js - Dari Limit Vercel ke Parallel Sharding di GitHub Actions

Scaling Konten MDX di Next.js - Dari Limit Vercel ke Parallel Sharding di GitHub Actions

Bagaimana saya menskalakan blog Next.js dari 1.000 ke 12.000+ file MDX dengan menyelesaikan limit ukuran fungsi serverless Vercel dan membangun pipeline kompilasi konten paralel dengan sharding di GitHub Actions

Arman Dwi Pangestu
Arman Dwi PangestuAugust 26, 2026
0 views
15 min read

Blog Next.js Anda berjalan dengan baik di 500 file MDX. Di 5.000 mulai melambat. Di 12.000 rusak total.

Vercel menabrak limit ukuran fungsi serverless 250 MB-nya. GitHub Actions runner OOM kill di 7 GB RAM. Satu kali next build memakan waktu lebih dari satu jam. Pipeline MDX yang tadinya bekerja dengan indah untuk situs portofolio menjadi bottleneck terbesar untuk platform konten.

Ini adalah cerita bagaimana saya menskalakan dari 1.000 ke 12.583 file MDX di stack Next.js + Velite -- menabrak dua dinding berbeda dan menyelesaikan masing-masing dengan cara berbeda. Jika Anda mengelola situs Next.js yang kontennya padat dan melihat waktu build meningkat, artikel ini membahas teknik tepat yang mengembalikan saya ke deploy di bawah 5 menit.

Ketika Blog Anda Melampaui Infrastruktur

Situs saya awalnya adalah blog Next.js standar dengan segelintir postingan MDX. Seiring waktu, berkembang menjadi platform pembelajaran bilingual dengan 472 series mencakup dua locale (Inggris dan Indonesia). Berikut isi direktori konten saat ini:

Tipe KontenFile
Episode series11.750
Metadata series472
Postingan blog285
Proyek43
Halaman legal4
Halaman guide29
Total file MDX12.583

Total konten di disk: ~120 MB sumber MDX, ~150 MB aset thumbnail.

Scaling terjadi dalam dua fase, masing-masing menabrak limit infrastruktur yang berbeda:

  • Fase 1 (1.000 - 3.000 file): Limit ukuran fungsi serverless Vercel mulai gagal
  • Fase 2 (3.000 - 12.000+ file): Kompilasi itu sendiri menjadi bottleneck -- RAM, waktu, dan keandalan

Memahami fase Anda menentukan solusi yang dibutuhkan.

Fase 1: Dinding Serverless 250 MB

Cara Vercel Bundle Bekerja

Vercel mendeploy Next.js sebagai fungsi serverless. Setiap route menjadi satu bundle fungsi. Vercel menggunakan @vercel/nft (Node File Traces) untuk menentukan file apa yang dibutuhkan setiap fungsi, lalu menggabungkannya.

Masalahnya adalah nft bersifat konservatif. Ketika melihat dynamic import atau akses filesystem yang tidak bisa di-resolve secara statis, ia cenderung menyertakan lebih banyak file daripada yang kurang. Di skala kecil ini tidak masalah. Di 12.000 file MDX, ini menjadi bencana.

Masalah JSON 152 MB

Velite mengkompilasi semua file MDX menjadi satu file JSON gabungan. Untuk situs saya, seriesEpisodes.json berukuran 152 MB -- 124 MB konten MDX yang sudah dikompilasi ditambah 28 MB metadata.

Karena file ini di-import oleh aplikasi, nft men-trace-nya ke setiap fungsi serverless yang secara transitif bergantung padanya. Setiap bundle fungsi tiba-tiba membawa 152 MB data konten, meskipun fungsi tersebut hanya membutuhkan segelintir series.

Masalah public/ Tree 906 MB

Lebih buruk lagi, saya memiliki logika resolusi thumbnail yang berjalan saat request time:

typescript
// Pemeriksaan runtime ini adalah bencana untuk serverless bundling
const hasThumb = fs.existsSync(
  `${process.cwd()}/public/series/${slug}.jpg`
)

nft Vercel tidak bisa resolve path dinamis yang dibangun dari variabel runtime. Ketika menemukan fs.existsSync() dengan path yang dikomputasi, ia menyerah mencoba menentukan file mana yang benar-benar dibutuhkan dan menggabungkan seluruh direktori public/ -- semua 906 MB-nya -- ke setiap fungsi.

Ditambah dengan JSON 152 MB, bundle fungsi individual melampaui limit ukuran fungsi serverless Vercel 250 MB. Deploy mulai gagal.

Solusi Tiga Bagian

1. Bagi JSON monolitik menjadi file per-series

Daripada satu seriesEpisodes.json sebesar 152 MB, saya menulis script post-compile yang memecahnya menjadi ~472 file individual:

plaintext
public/static/series-episodes/en/learn-docker.json      (50-300 KB masing-masing)
public/static/series-episodes/en/learn-kubernetes.json
public/static/series-episodes/id/learn-docker.json
...

File gabungan dihapus setelah dipecah. Karena nft hanya men-trace file yang benar-benar di-import, dan file JSON individual dimuat on-demand per halaman series, setiap bundle fungsi hanya membawa konten yang benar-benar dibutuhkan.

2. Strip body dari collection JSON

Collection posts dan projects dipecah menjadi dua tier:

  • File gabungan meta-only (title, slug, excerpt, dates) -- ~2 MB total, digunakan oleh halaman list/index
  • Full body payloads di public/static/ -- hanya dimuat di halaman detail post/project individual

Ini mengurangi data collection yang di-trace dari 152 MB menjadi 2 MB.

3. Resolusi thumbnail saat build time

typescript
// Sebelum: fs.existsSync runtime membuat nft bundle public/ tree
const hasThumb = fs.existsSync(`${process.cwd()}/public/series/${slug}.jpg`)
 
// Sesudah: di-resolve selama Velite computedFields, hasil di-bake ke JSON
// nft tidak pernah menyentuh filesystem saat runtime

Dengan meng-resolve keberadaan thumbnail selama langkah kompilasi Velite (di computedFields), path yang sudah di-resolve ditulis langsung ke output JSON. Fungsi serverless tidak perlu lagi mengecek filesystem.

Hasil

Bundle fungsi serverless turun dari 250+ MB menjadi di bawah 15 MB. Deploy Vercel berhasil kembali.

Tapi ini hanya menyelesaikan setengah masalah. Konten bisa di-deploy, tapi mengompilasinya menjadi semakin lambat dan tidak bisa diandalkan setiap minggu.

Masalah OOM Episode

Setelah menyelesaikan masalah ukuran bundle, next build mulai crash dengan error berbeda: JavaScript heap out of memory selama fase "Collecting page data".

Penyebabnya adalah seriesEpisodes.json yang sama -- sekarang sudah 173 MB seiring pertumbuhan konten. Setiap halaman detail episode secara statis meng-import collection penuh di module scope, dan generateStaticParams() mengembalikan semua 3.400+ episode. Selama build, V8 workers meng-parse seluruh JSON untuk setiap route, menghabiskan heap memory.

Fix-nya adalah arsitektur tiga bagian:

1. Collection meta ringan

Collection seriesEpisodesMeta terpisah mengkompilasi file MDX yang sama tapi tanpa field body -- tanpa kompilasi MDX, tanpa Shiki highlighting. Output: ~4 MB alih-alih 173 MB. Halaman list, generateStaticParams, dan generateMetadata semuanya membaca dari collection meta.

2. Asset CDN per-series

Daripada satu JSON gabungan, script post-compile membagi episode menjadi satu file per-series (public/static/series-episodes/{lang}/{series}.json, ~2.5 MB masing-masing). Halaman detail episode hanya mengambil file series-nya sendiri via fetch() dengan ISR caching 24 jam. File gabungan dihapus setelah dibagi -- tidak pernah masuk ke module graph Next.js.

3. Dynamic rendering untuk detail routes

Detail routes (blog, projects, episodes) menggunakan force-dynamic alih-alih static pre-rendering. Ini mengeliminasi fase "Collecting page data" yang rentan OOM untuk 3.400+ route, mengurangi route yang di-pre-render dari ~3.500 menjadi ~540, dan menjaga bundle halaman episode tetap di ~96 KB tanpa data episode yang di-trace ke fungsi serverless manapun.

Fase 2: Ketika Kompilasi Itu Sendiri Rusak

Bottleneck Sebenarnya

Dengan masalah ukuran bundle terselesaikan, masalah berikutnya menjadi mustahil untuk diabaikan: mengkompilasi 12.000 file MDX melalui pipeline Velite itu lambat dan boros memori.

Setiap file MDX melewati pipeline pemrosesan yang berat:

  • Shiki syntax highlighting dengan dual theme (one-light + one-dark-pro)
  • TypeScript twoslash untuk anotasi contoh kode
  • Rehype plugins: konversi diagram mermaid, code groups, notation diff/highlight/focus, autolink headings
  • Frontmatter parsing dengan computed fields (resolusi thumbnail, normalisasi author)

Dampak terukur di GitHub Actions runner 7 GB:

MetrikNilai
Kompilasi tunggal (semua 12.583 file)30-60 menit
Peak RAM per 2.000 file MDX~3.5 GB RSS
Threshold OOM kill~5.5 GB V8 heap
Limit build Vercel45 menit

Di 12.000+ file dalam satu proses Velite, V8 heap tumbuh melampaui RAM yang tersedia dan proses di-OOM kill. Bahkan di mesin dengan RAM cukup, waktu kompilasi melampaui limit 45 menit Vercel.

Mengapa Incremental Build Bukan Pilihan

Velite (dan sebagian besar kompilator MDX) tidak memiliki kompilasi incremental yang sesungguhnya. Setiap build memproses semua file dari awal. Tidak ada state intermediate yang di-cache, tidak ada output diferensial, tidak ada rebuild parsial. Satu typo fix di satu file MDX memicu recompile penuh dari semua 12.000 file.

Ini adalah limit fundamental dari model kompilasi MDX. Plugin chain rehype dan remark beroperasi pada full document tree, dan Shiki perlu memuat definisi grammar dan theme terlepas dari berapa file yang benar-benar berubah.

Solusi: Content Sharding

Jawabannya adalah horizontal scaling. Daripada mengkompilasi semua 12.000 file dalam satu proses, pecah menjadi kelompok lebih kecil (shards), kompilasi setiap shard secara independen, jalankan secara paralel di runner CI terpisah, lalu gabungkan outputnya.

Arsitektur Sharding

Cara Shard Planner Bekerja

Script kustom (generate-shards.ts) menganalisis direktori konten dan menghasilkan rencana shard:

100%

Algoritmanya sederhana:

  1. Urutkan semua series berdasarkan locale, lalu alphabetically berdasarkan slug
  2. Akumulasikan series ke dalam shard sampai penambahan series berikutnya akan melampaui limit 2.000 file
  3. Ketika limit tercapai (atau locale berubah), finalisasi shard saat ini dan mulai yang baru
  4. Namai shard berdasarkan locale dan index: en-1, en-2, id-1, dst.

Keputusan desain kunci:

Granularitas per-series -- Satu series tidak pernah dipecah lintas shard. Jika learn-graphql punya 51 episode, semua 51 tetap di shard yang sama. Ini menjaga merge tetap sederhana dan menghindari dependensi lintas shard.

Enforcement batas locale -- Shard tidak pernah mencampur konten Inggris dan Indonesia. Ini di-enforce oleh deteksi perubahan locale di planner. Artinya en-1 hanya berisi series Inggris, dan id-1 hanya berisi series Indonesia.

Penugasan konten bersama -- Postingan blog, proyek, guide, dan halaman legal hanya ditugaskan ke shard pertama setiap locale. Blog dan proyek memiliki subdirektori per-locale (en/, id/), jadi terpecah secara natural. Konten hanya-Inggris (guide/, legal/) masuk ke en-1.

Jumlah shard dinamis -- Jumlah shard tidak di-hardcode. Algoritma menghasilkan shard sebanyak yang dibutuhkan. Hari ini 7; jika konten tumbuh ke 20.000 file, otomatis menjadi ~10.

Layout Shard Saat Ini

ShardSeriesFileKonten
en-182~2.000Series Inggris + blog EN + proyek EN + guide + legal
en-282~2.000Series Inggris
en-315~400Series Inggris
id-178~2.000Series Indonesia + blog ID + proyek ID
id-278~2.000Series Indonesia
id-380~2.000Series Indonesia
id-457~1.400Series Indonesia

Struktur Package Shard

Setiap shard adalah package npm yang mandiri di bawah packages/content-{locale}-{N}/. Menggunakan symlink untuk mereferensikan direktori konten kanonik, menghindari duplikasi di disk:

plaintext
packages/content-en-1/
  content/
    blog/posts/en -> symlink ke content/blog/posts/en
    projects/en   -> symlink ke content/projects/en
    guide         -> symlink ke content/guide
    legal         -> symlink ke content/legal
    series/en/
      learn-docker      -> symlink ke content/series/en/learn-docker
      learn-docker.mdx  -> symlink ke content/series/en/learn-docker.mdx
  velite.config.ts    # satu baris import shared config
  package.json
  shard.ts

Config Velite untuk setiap shard hanya satu baris:

typescript
import { createShardConfig } from "../shared/velite/config"
export default createShardConfig("content")

Seluruh konfigurasi Velite -- collections, MDX plugins, Shiki themes, rehype plugins -- terpusat di packages/shared/. Mengubah pipeline MDX memperbarui semua shard secara bersamaan.

Pipeline CI

Arsitektur Pipeline Lengkap

100%

Langkah 1: Drift Gate

Sebelum kompilasi dimulai, setup job memvalidasi integritas shard:

bash
bun script/generate-shards.ts --check

Ini menangkap tiga kategori masalah:

  • Series orphan: konten ada di content/ tapi tidak ditugaskan ke shard manapun
  • Series duplikat: series yang sama ditugaskan ke beberapa shard (yang akan menyebabkan korupsi data saat merge)
  • Symlink rusak: package shard mereferensikan konten yang sudah tidak ada

Jika pemeriksaan gagal, seluruh pipeline dibatalkan. Tidak ada runner yang terbuang untuk layout yang rusak. Ini adalah safeguard kritis -- tanpanya, Anda mungkin mengkompilasi 7 shard hanya untuk menemukan saat merge bahwa 300 item terduplikasi.

Langkah 2: Dynamic Matrix

Matrix dihitung dari rencana shard secara runtime, bukan di-hardcode di workflow:

yaml
- name: Compute shard matrix
  id: matrix
  run: |
    MATRIX=$(bun script/generate-shards.ts --json | jq -c '[.[].name]')
    echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT"

Ketika konten tumbuh dan algoritma menghasilkan shard ke-8, matrix secara otomatis memasukkannya. Tidak perlu edit file workflow. Ini mengeliminasi seluruh kategori kesalahan maintenance.

Langkah 3: Kompilasi Per-Shard

Setiap matrix runner mengkompilasi satu shard. Prosesnya:

100%

Setiap shard diberi limit V8 heap 5 GB di runner 7 GB, menyisakan ruang untuk OS dan proses lain:

yaml
env:
  NODE_OPTIONS: "--max-old-space-size=5120"

Waktu kompilasi per-shard:

ShardFileWaktu
en-12.000~8 menit
en-22.000~8,5 menit
en-3400~1,5 menit
id-12.000~8,5 menit
id-22.000~9,5 menit
id-32.000~8 menit
id-41.400~4 menit

Wall-clock time dengan 7 runner paralel: ~10 menit (terhambat oleh shard terlambat). Bandingkan dengan 30-60 menit untuk kompilasi monolitik di satu runner.

Langkah 4: Vercel Build

Vercel tidak pernah menjalankan Velite. Build script mengunduh shard yang sudah dikompilasi dari R2 dan menjalankan next build:

typescript
// vercel-build.ts (simplified)
async function main() {
  // 1. Download shard yang sudah dikompilasi dari R2
  execSync("bun script/download-shards.ts")
 
  // 2. next build (data konten sudah dikompilasi)
  execSync("bun run build:next")
 
  // 3. Sync static assets ke R2
  execSync("bun run assets:sync:static")
}

Download script menggabungkan semua output shard ke satu direktori .velite/ dengan deduplikasi ketat. Setiap item di-key oleh kombinasi locale, series slug, dan episode slug. Item duplikat lintas shard menyebabkan error keras untuk mencegah korupsi data:

100%

Seluruh Vercel build (download + merge + next build) selesai di bawah 5 menit.

Artifact Storage di Cloudflare R2

Mengapa R2

Saya memilih Cloudflare R2 untuk penyimpanan artifact shard karena tiga alasan:

Tanpa egress fees -- Berbeda dengan AWS S3, R2 tidak mengenakan biaya egress. Ini penting ketika Vercel mengunduh 7 archive shard di setiap deploy. Di skala besar, biaya egress di S3 akan bertambah cepat.

API S3-compatible -- R2 bekerja dengan rclone out of the box menggunakan protocol S3. Tidak ada vendor lock-in, tidak ada SDK proprietary. Perintah rclone yang sama bekerja dengan S3, MinIO, atau storage S3-compatible lainnya.

Edge network -- Edge network Cloudflare menyediakan download latency rendah dari region Vercel manapun. Archive shard biasanya 50-200 MB masing-masing, dan terdownload dalam hitungan detik.

Struktur Bucket R2

plaintext
devvnull-content-shards/
  content-shards/
    en-1/
      a1b2c3d4e5f6.tar.gz    # archive shard yang sudah dikompilasi
    en-2/
      f6e5d4c3b2a1.tar.gz
    ...
  content-builds/
    {hash}.tar.gz              # build monolitik (release workflow)
  static/
    production/                # static assets per-environment
    preview/

Content-Addressable Caching

Setiap archive shard dinamai berdasarkan content hash-nya -- SHA-256 dari semua file sumber MDX dan config Velite dalam shard tersebut. Jika konten tidak berubah, hash-nya sama, dan runner CI melewati kompilasi sepenuhnya.

typescript
// build-shard.ts
const hash = computeContentHash(shardDir)
const cacheKey = `${SHARD_NAME}-${hash}.tar.gz`
 
// Cek local cache dulu, lalu R2
if (existsSync(`.cache/shards/${cacheKey}`)) {
  console.log("cache hit, skipping compile")
  return
}

Dengan 7 shards, mengedit postingan blog di en-1 hanya mengkompilasi shard tersebut (~8 menit). 6 shard lainnya adalah cache hit dan melewati kompilasi sepenuhnya.

Struktur Konten untuk Multi-Locale Sharding

Direktori konten menggunakan struktur berbasis locale yang terpeta secara natural ke sharding:

plaintext
content/
  series/
    en/                          # 179 series Inggris
      learn-docker/
        episode-0.mdx
        episode-1.mdx
        ...
      learn-docker.mdx           # Metadata series
    id/                          # 293 series Indonesia
      learn-docker/
        ...
  blog/
    posts/
      en/                        # 120 postingan blog Inggris
      id/                        # 165 postingan blog Indonesia
  projects/
    en/                          # 24 proyek Inggris
    id/                          # 19 proyek Indonesia
  legal/                         # 4 halaman legal hanya-Inggris
  guide/                         # 29 halaman dokumentasi hanya-Inggris

Shard planner menggunakan struktur locale ini untuk menegaskan batas. Series diurutkan berdasarkan locale dulu, lalu alphabetically. Ketika planner menemukan perubahan locale, ia finalisasi shard saat ini dan mulai yang baru. Ini memastikan tidak ada shard yang pernah mencampur konten Inggris dan Indonesia.

Setiap shard hanya mensymlink konten yang ditugaskan kembali ke direktori konten kanonik content/. Ini menghindari duplikasi 120 MB file MDX di 7 package shard di disk.

Yang Salah (Dan Cara Menghindarinya)

1. fs.existsSync() Runtime di Fungsi Serverless

Ini adalah kesalahan paling menyakitkan. Satu baris kode yang mengecek keberadaan thumbnail saat request time membuat file tracer Vercel menggabungkan 906 MB ke setiap fungsi.

Pelajarannya: di lingkungan serverless, jangan pernah mengakses filesystem saat runtime dengan path dinamis. Resolve semuanya saat build time dan bake hasilnya ke data file Anda.

2. JSON Konten Monolitik

Membiarkan Velite menghasilkan satu seriesEpisodes.json sebesar 152 MB tampak praktis. Ini adalah bencana untuk deployment serverless. Setiap fungsi mendapat full 152 MB tergabung di dalamnya.

Fix-nya sederhana: bagi menjadi file kecil per-series dan hapus file gabungan. Tradeoff-nya adalah lebih banyak HTTP request saat runtime (satu per series), tapi setiap request kecil dan bisa di-cache.

3. Duplikasi Konten Bersama Lintas Shard

Ketika saya pertama kali mengimplementasikan sharding, saya mensymlink seluruh direktori content/blog/ ke kedua shard en-1 dan id-1. Ini menghasilkan 332 item duplikat lintas shard. Langkah merge mendeteksi duplikasi dan gagal fatal, yang menyebabkan Vercel build retry dalam loop tak terbatas.

Fix-nya: bagi konten bersama berdasarkan locale. en-1 mendapat blog/posts/en/ dan projects/en/. id-1 mendapat blog/posts/id/ dan projects/id/. Konten hanya-Inggris (guide/, legal/) masuk ke en-1 saja.

4. Tidak Ada CI Drift Detection

Awalnya saya memelihara penugasan shard secara manual. Ketika series baru ditambahkan ke content/, tidak selalu ditugaskan ke shard. Konten diam-diam hilang dari situs yang di-deploy.

Fix-nya: generate-shards.ts --check sebagai CI gate. Ia exit non-zero pada series orphan, duplikasi, atau symlink rusak. Ini berjalan sebelum kompilasi dimulai, jadi layout yang rusak langsung terdeteksi.

5. Matrix Hardcoded di GitHub Actions

Awalnya saya mencantumkan nama shard secara manual di matrix workflow. Setiap kali jumlah shard berubah, saya harus edit file workflow. Ini rentan kesalahan dan mengalahkan tujuan automasi.

Fix-nya: hitung matrix secara dinamis dari output --json. Workflow tidak pernah perlu update manual ketika konten tumbuh.

Best Practices

Cache Secara Agresif

Caching berbasis content hash berarti shard yang tidak berubah melewati kompilasi sepenuhnya. Dengan 7 shards, sebagian besar push hanya mengkompilasi 1-2 shard. Shard lainnya adalah cache hit. Ini adalah kemenangan terbesar untuk velocity development sehari-hari.

Gunakan fail-fast: false

yaml
strategy:
  fail-fast: false

Jika satu shard gagal, biarkan yang lain selesai. Ini memberikan hasil parsial dan memudahkan debugging. Anda tahu persis shard mana yang rusak dan bisa menginspeksi outputnya tanpa menjalankan ulang seluruh pipeline.

Set Limit Memori Secara Eksplisit

yaml
env:
  NODE_OPTIONS: "--max-old-space-size=5120"

GitHub Actions ubuntu-latest runner memiliki 7 GB RAM. Mengatur limit V8 heap 5 GB menyisakan ruang untuk OS dan proses lain. Tanpa ini, Node.js mungkin mengalokasikan lebih banyak memori daripada yang bisa ditangani runner, menyebabkan OOM kill.

Pertahankan Velite Config Terpusat

Semua package shard import dari packages/shared/velite/config. Satu sumber kebenaran untuk collections, plugins, dan output configuration. Ketika Anda upgrade Shiki atau menambahkan rehype plugin baru, Anda ubah satu file dan semua shard langsung memperbarui.

Pisahkan Kompilasi Konten dari App Build

Kompilasi Velite (pemrosesan konten) dan next build (kompilasi aplikasi) adalah langkah terpisah. Pemisahan inilah yang memungkinkan seluruh arsitektur. Kompilasi konten di runner CI yang kuat dengan 7 GB RAM dan tanpa limit waktu build. Build aplikasi di lingkungan Vercel yang terbatas di mana next build selesai dalam hitungan menit karena data konten sudah dikompilasi.

Content-Aware Build Cache

Bahkan dengan sharding, kompilasi Velite penuh dari 12.000+ file memakan waktu 30-60 menit. Cache content-aware mengeliminasi kompilasi yang tidak perlu sepenuhnya.

Script build menghitung hash SHA-256 dari semua file content/**, velite.config.ts, plugin velite/**, dan bun.lock. Pada cache hit, direktori .velite/ dan public/static/ yang sudah dikompilasi dipulihkan dan Velite tidak pernah dipanggil. Pada cache miss, Velite berjalan dan output di-snapshot untuk waktu berikutnya.

Dalam praktiknya: cold build 859 detik (14:21) turun menjadi cache hit 0,2 detik. Mengedit postingan blog di en-1 hanya mengkompilasi shard tersebut; 6 shard lainnya adalah cache hit dan melewati kompilasi sepenuhnya. Di Vercel, Turborepo dengan remote caching menyediakan manfaat yang sama -- konten yang tidak berubah dipulihkan dari cache dalam waktu kurang dari satu detik.

Pipeline Dev yang Lebih Murah

Pengembangan lokal dengan pipeline Shiki + twoslash penuh lambat karena Velite men-tokenize ~13.000 blok kode setiap cold start. Config dev-only menggantikan rehype-pretty-code dengan plugin passthrough ringan yang mempertahankan struktur kode (layout, atribut data, code groups) tapi melewati syntax highlighting sepenuhnya.

Dengan config ini, edit satu file dalam watch mode rebuild dalam ~5 detik alih-alih menjalankan ulang pipeline penuh. Build produksi mempertahankan Shiki highlighting lengkap. Trade-off-nya adalah preview lokal menampilkan kode tanpa warna token, tapi layout dan strukturnya identik.

Kapan TIDAK Menggunakan Ini

Sharding berlebihan ketika:

  • Di bawah 2.000 file MDX -- Kompilasi Velite tunggal biasanya selesai dalam 5-10 menit. Anda tidak butuh kompilasi paralel.
  • Tidak ada tekanan waktu CI -- Jika Anda tidak masalah menunggu 30+ menit untuk build, sharding menambah kompleksitas tanpa manfaat proporsional.
  • Single locale -- Sharding lintas locale adalah pilihan natural. Sharding dalam satu locale juga bisa, tapi ROI-nya lebih rendah.

Limitasi pendekatan ini:

  • Series tidak bisa dipecah -- Jika satu series memiliki lebih dari 2.000 episode, ia tidak bisa di-shard. Series terbesar di konten saya saat ini punya 51 episode, masih jauh di bawah batas.
  • Tidak ada incremental rebuild -- Setiap shard tetap mengkompilasi semua file dari awal. Parallelism mengurangi wall-clock time, bukan total compute time.
  • Kompleksitas merge -- Langkah download dan merge harus menangani deduplikasi dan resolusi konflik. Ini adalah titik kegagalan potensial yang membutuhkan pengujian hati-hati.
  • Ketergantungan R2 -- Arsitektur membutuhkan Cloudflare R2 (atau setara) untuk penyimpanan artifact. Ini menambah biaya infrastruktur dan mode kegagalan.

Alternatif yang perlu dipertimbangkan:

  • Contentlayer -- Model kompilasi serupa, masalah scaling yang sama. Juga saat ini tidak dirawat.
  • CMS berbasis database -- Mengalihkan kompilasi sepenuhnya tapi kehilangan pengalaman authoring MDX.
  • Astro Content Collections -- Penanganan konten bawaan yang lebih baik untuk proyek baru, tapi biaya migrasi tinggi untuk situs Next.js yang sudah ada.

Self-Hosted Runner dengan Multipass

Sebelum memilih sharding, saya menguji runner GitHub Actions self-hosted menggunakan Multipass di ThinkPad T14 Gen 2i (Intel i7-1185G7, 32 GB RAM). VM menjalankan Ubuntu 24.04 dengan 4 vCPU dan 23 GB RAM -- cukup headroom untuk kompilasi full tree Velite.

Hasilnya mengonfirmasi kurva scaling:

RunnerRAMKompilasi (11k file)Hasil
GitHub-hosted (ubuntu-latest)7 GBOOM killTidak bisa mengkompilasi full tree
Self-hosted VM (Multipass)23 GB~65 menitBerhasil, tapi lambat
Self-hosted VM (8 vCPU)23 GB~40 menitLebih cepat, masih viable
Shard paralel (7 runner)7 GB masing-masing~10 menit wall-clockSolusi yang dipilih

Pendekatan self-hosted bekerja untuk set konten yang lebih kecil atau server build khusus, tapi membutuhkan mesin yang menyala selama CI. Persyaratan RAM berskala linear: Velite menahan seluruh AST MDX + token Shiki di memory sebelum menulis output, pada sekitar 1 MB RSS per file. Kompilasi full tree mencapai peak 18,7 GB RSS.

Pelajaran kunci: NODE_OPTIONS membantu untuk V8 heap tapi tidak untuk total RSS. Mengatur --max-old-space-size=5120 membatasi heap JavaScript, tapi memory native (Shiki grammars, AST, tokenization) mendorong RSS jauh lebih tinggi. Swap tersedia tapi membunuh performa -- kompilasi 3-5x lebih lambat ketika RSS melebihi RAM fisik.

Sharding adalah pilihan pragmatis: menggunakan 7 runner GitHub-hosted yang sama yang sudah tersedia, mengkompilasi secara paralel, dan cache R2 berarti konten yang tidak berubah melewati kompilasi sepenuhnya.

Kesimpulan

  1. Limit serverless 250 MB Vercel itu nyata. JSON konten monolitik dan akses filesystem runtime akan meledakkannya. Bagi konten menjadi file kecil dan resolve semuanya saat build time.

  2. Kompilasi MDX tidak scale secara vertikal. Di ~2.000 file per proses, V8 heap pressure menjadi bottleneck. Horizontal scaling melalui sharding adalah solusi pragmatis.

  3. Masalah OOM episode terpisah dari masalah ukuran bundle. Setelah membagi JSON monolitik, collection 173 MB masih bisa OOM build saat di-import di module scope. Gunakan collection meta-only untuk list/params, file per-series yang dilayani CDN untuk body, dan dynamic rendering untuk detail routes.

  4. Content-aware caching mengeliminasi kompilasi yang tidak perlu. Cache content-hash berarti shard yang tidak berubah melewati Velite sepenuhnya -- cold build turun dari 14 menit menjadi 0,2 detik pada cache hit.

  5. Pipeline dev yang murah menjaga iterasi tetap cepat. Menggantikan Shiki dengan plugin passthrough selama pengembangan berarti edit satu file rebuild dalam ~5 detik alih-alih menjalankan ulang pipeline penuh.

  6. Arsitekturnya: Konten di git, shard planner, kompilasi CI paralel, R2 artifact storage, Vercel download dan merge, deploy cepat.

  7. Automasi mencegah drift. CI gate --check dan dynamic matrix berarti maintenance shard zero-touch. Tambah konten, push, langsung jalan.

Jika Anda mengalami limit Vercel build, mulai dengan fix body-splitting dan resolusi thumbnail sebelum melompat ke sharding. Jika Anda butuh pipeline sharding penuh, teknik dalam artikel ini sudah teruji di 12.000+ file MDX. Mulai dari fix terkecil yang menangani bottleneck Anda saat ini dan tambahkan kompleksitas hanya ketika dibutuhkan.


Related Posts