Blob Objects di JavaScript: Panduan Praktis
TutorialJavaScriptFrontend#tutorial#performance#web development#javascript

Blob Objects di JavaScript: Panduan Praktis

A
Abd. Asis
Bagikan:

Saat menangani file di browser — mulai dari upload gambar besar, export data ke CSV, hingga preview PDF sebelum dikirim ke server — Blob adalah primitif yang hampir selalu terlibat meski jarang dibicarakan secara eksplisit. Banyak developer memakainya hanya sebagai perantara dari fetch().then(r => r.blob()), padahal Blob punya kemampuan yang jauh lebih luas dan sering jadi kunci performa aplikasi yang banyak berurusan dengan data biner.

Artikel ini membahas Blob secara praktis: apa itu, kapan dipakai, dan pola-pola yang benar untuk use case nyata seperti chunked upload, kompresi gambar via canvas, preview file, export data, serta manajemen memori agar aplikasi tidak bocor di sesi panjang.

Apa Itu Blob dan Kenapa Bukan Data URL

Blob (Binary Large Object) merepresentasikan data mentah yang immutable dan bisa diperlakukan seperti file — lengkap dengan ukuran dan MIME type — meskipun datanya tidak berasal dari filesystem. Objek ini adalah bagian dari File API yang didukung semua browser modern.

Pendekatan lama untuk menaruh data biner di halaman adalah Data URL (data:image/png;base64,...). Kelihatan sederhana, tapi punya beberapa masalah serius: proses encoding base64 membengkakkan ukuran sekitar 33%, string panjangnya ikut masuk ke DOM atau CSS, dan sebagian browser membatasi panjang URL. Untuk gambar kecil mungkin masih aman, tapi begitu menyentuh beberapa MB, halaman akan terasa berat saat parsing.

Blob menyelesaikan masalah itu dengan cara berbeda. Data disimpan di memori browser sebagai binary, lalu diakses lewat object URL pendek seperti blob:https://situs.com/uuid-abc. URL ini hanya pointer — konten sebenarnya tidak ikut tertulis di DOM.

Blob adalah parent class dari File. Artinya setiap File yang kamu dapat dari <input type="file"> sebenarnya juga Blob, lengkap dengan method seperti slice(), text(), dan arrayBuffer().

Membuat Blob dari Berbagai Sumber

Constructor Blob menerima array berisi potongan data (string, ArrayBuffer, TypedArray, atau Blob lain) dan objek opsi berisi type untuk MIME type. Kebiasaan yang baik adalah selalu menentukan type secara eksplisit, karena banyak API browser (misalnya URL.createObjectURL) bergantung padanya untuk menentukan cara merender konten.

Agar konsisten, kita bisa membungkus pembuatan Blob ke dalam helper kecil sesuai konteks. Contohnya untuk sebuah dashboard analitik yang sering meng-export laporan:

JavaScriptJS
// resources/js/lib/blob-makers.js
export function buildJsonReport(report) {
  const serialized = JSON.stringify(report, null, 2)
  return new Blob([serialized], { type: 'application/json' })
}

export function buildCsvReport(rows) {
  const escape = (value) => {
    const str = String(value ?? '')
    return /[",\n]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str
  }

  const body = rows.map((row) => row.map(escape).join(',')).join('\n')
  return new Blob([body], { type: 'text/csv;charset=utf-8' })
}

export function buildPlainNotes(lines) {
  return new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' })
}

Helper di atas memperlakukan pembuatan Blob sebagai batas yang jelas: di satu sisi kita punya data JavaScript biasa, di sisi lain kita punya artefak file yang siap dipakai. Fungsi escape pada buildCsvReport penting karena nilai yang mengandung koma, kutip, atau newline harus di-quote agar tidak merusak format CSV.

Memotong File Besar dengan slice()

Salah satu fitur paling berguna dari Blob adalah method slice(start, end, contentType) yang bekerja mirip Array.prototype.slice. Hasilnya adalah Blob baru yang hanya mereferensikan potongan dari data asli — tanpa menyalin byte ke memori baru. Ini dasar dari chunked upload yang memungkinkan file besar dikirim sedikit demi sedikit.

Bayangkan sebuah aplikasi manajemen dokumen internal yang menerima scan PDF berukuran puluhan MB. Mengirim satu request raksasa beresiko timeout dan tidak bisa di-resume kalau koneksi putus. Lebih aman memotongnya:

JavaScriptJS
// resources/js/features/uploader/chunked-upload.js
const CHUNK_SIZE = 2 * 1024 * 1024 // 2 MB

export async function uploadDocumentInChunks(file, onProgress) {
  const transferId = crypto.randomUUID()
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE)

  for (let index = 0; index < totalChunks; index++) {
    const start = index * CHUNK_SIZE
    const end = Math.min(start + CHUNK_SIZE, file.size)
    const segment = file.slice(start, end, file.type)

    const payload = new FormData()
    payload.append('transfer_id', transferId)
    payload.append('chunk_index', String(index))
    payload.append('total_chunks', String(totalChunks))
    payload.append('segment', segment, file.name)

    await fetch('/api/documents/chunks', {
      method: 'POST',
      body: payload,
    })

    onProgress?.({
      sent: index + 1,
      total: totalChunks,
      percent: Math.round(((index + 1) / totalChunks) * 100),
    })
  }

  return transferId
}

Yang penting dicatat di sini: file.slice() tidak menduplikasi data di memori — browser cukup menyimpan offset. Server kemudian menggabungkan semua chunk berdasarkan transfer_id dan chunk_index. Kalau koneksi putus di chunk ke-7, kamu cukup mengulang dari chunk ke-7 dengan transfer_id yang sama, tidak perlu mengulang dari awal.

Kompresi Gambar dengan Canvas dan toBlob

Upload foto dari kamera smartphone bisa dengan mudah melebihi 5 MB per file, padahal untuk kebutuhan thumbnail atau avatar cukup beberapa ratus KB saja. Alih-alih membebani server dengan gambar mentah, lebih baik mengecilkannya dulu di browser. canvas.toBlob() adalah kuncinya karena mengembalikan hasil dalam bentuk Blob yang siap di-upload.

Berikut contoh fungsi yang men-resize gambar ke dimensi maksimal tertentu lalu mengkompresnya sebagai JPEG:

JavaScriptJS
// resources/js/features/uploader/image-compressor.js
export async function compressImage(file, { maxWidth = 1280, quality = 0.82 } = {}) {
  const bitmap = await createImageBitmap(file)
  const scale = Math.min(1, maxWidth / bitmap.width)
  const targetWidth = Math.round(bitmap.width * scale)
  const targetHeight = Math.round(bitmap.height * scale)

  const canvas = document.createElement('canvas')
  canvas.width = targetWidth
  canvas.height = targetHeight

  const ctx = canvas.getContext('2d')
  ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight)
  bitmap.close()

  const compressed = await new Promise((resolve) => {
    canvas.toBlob(resolve, 'image/jpeg', quality)
  })

  return {
    blob: compressed,
    originalSize: file.size,
    compressedSize: compressed.size,
    ratio: 1 - compressed.size / file.size,
  }
}

Dua hal yang layak dipahami. Pertama, createImageBitmap() lebih efisien daripada new Image() + onload karena bekerja di thread terpisah dan hasilnya bisa langsung digambar ke canvas. Kedua, toBlob() asynchronous dan menerima callback — dibungkus Promise agar bisa di-await dengan nyaman. Nilai quality berada di rentang 0–1 dan 0.82 biasanya jadi titik seimbang antara kualitas visual dan ukuran file.

Preview File dengan Object URL

Saat user memilih file dari <input type="file">, sering kita ingin menampilkan preview sebelum file benar-benar dikirim. Di sinilah URL.createObjectURL(blob) berperan — ia mengembalikan URL pendek yang bisa langsung dipasang ke atribut src <img>, <video>, atau <iframe>.

JavaScriptJS
// resources/js/features/preview/file-preview.js
export function createPreview(file) {
  const url = URL.createObjectURL(file)

  if (file.type.startsWith('image/')) {
    return { kind: 'image', url }
  }

  if (file.type.startsWith('video/')) {
    return { kind: 'video', url }
  }

  if (file.type === 'application/pdf') {
    return { kind: 'pdf', url }
  }

  return { kind: 'unknown', url }
}

Tampak sederhana, tapi ada jebakan memori yang harus diwaspadai.

Setiap URL.createObjectURL() yang dibuat akan terus menahan memori Blob yang dirujuknya sampai halaman di-reload atau kamu memanggil URL.revokeObjectURL(). Di aplikasi yang banyak membuat preview (misalnya uploader batch), ini bisa jadi penyebab kebocoran memori yang sulit dilacak.

Manajemen Object URL agar Tidak Bocor

Solusinya adalah memperlakukan object URL seperti resource yang punya lifecycle: dibuat, dipakai, lalu dibebaskan. Pola registry kecil cukup efektif untuk aplikasi skala menengah:

JavaScriptJS
// resources/js/lib/object-url-registry.js
const activeUrls = new Set()

export function trackUrl(blob) {
  const url = URL.createObjectURL(blob)
  activeUrls.add(url)
  return url
}

export function releaseUrl(url) {
  if (activeUrls.delete(url)) {
    URL.revokeObjectURL(url)
  }
}

export function releaseAllUrls() {
  for (const url of activeUrls) {
    URL.revokeObjectURL(url)
  }
  activeUrls.clear()
}

Di komponen React, pola ini biasanya dibungkus dengan useEffect agar URL otomatis di-revoke saat komponen unmount:

React JSXJSX
import { useEffect, useState } from 'react'

function AttachmentPreview({ file }) {
  const [previewUrl, setPreviewUrl] = useState(null)

  useEffect(() => {
    if (!file) return
    const url = URL.createObjectURL(file)
    setPreviewUrl(url)

    return () => {
      URL.revokeObjectURL(url)
    }
  }, [file])

  if (!previewUrl) return null

  return <img src={previewUrl} alt={`Preview ${file.name}`} className="rounded" />
}

Cleanup function di useEffect berjalan baik saat file berubah maupun saat komponen unmount, jadi tidak ada URL yatim yang menempel di memori.

Memicu Download dari Blob

Kasus terakhir yang sering muncul: tombol "Export" yang menghasilkan file unduhan langsung di browser tanpa perlu roundtrip ke server. Polanya selalu mirip — buat Blob, bungkus dengan object URL, lalu picu klik pada elemen <a> sementara.

JavaScriptJS
// resources/js/lib/download.js
export function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob)
  const anchor = document.createElement('a')
  anchor.href = url
  anchor.download = filename
  document.body.appendChild(anchor)
  anchor.click()
  anchor.remove()
  URL.revokeObjectURL(url)
}

Perhatikan bahwa URL.revokeObjectURL() dipanggil langsung setelah click(). Browser sudah memulai proses unduhan sebelum baris itu dieksekusi, jadi aman untuk segera membebaskan URL-nya. Dengan helper ini, export JSON atau CSV dari dashboard tinggal memanggil downloadBlob(buildCsvReport(rows), 'laporan.csv').

Kesimpulan

Memahami Blob mengubah cara kita berpikir tentang data biner di browser: bukan lagi sebagai sesuatu yang harus di-base64-kan ke DOM, tapi sebagai objek berumur pendek dengan lifecycle yang jelas. Begitu pola allocate–use–revoke menjadi kebiasaan, use case seperti chunked upload, kompresi gambar, preview file, dan export data akan terasa seperti variasi dari tema yang sama. Untuk eksplorasi lebih jauh, Streams API adalah langkah alami berikutnya — terutama ketika file yang diproses lebih besar dari memori yang tersedia.

Referensi

  1. 1 MDN Web Docs — Blob API
  2. 2 MDN Web Docs — URL.createObjectURL()
  3. 3 MDN Web Docs — HTMLCanvasElement.toBlob()
  4. 4 W3C File API Specification
Abd. Asis
Ditulis oleh
Abd. Asis

Software Developer dari Madura. Menulis tentang PHP, Laravel, dan pengembangan web modern dalam Bahasa Indonesia.