Trade-off Serialisasi Data URI Base64 dalam Respons JSON

Penyematan Low-Quality Image Placeholder (LQIP) langsung pada payload API menghilangkan round-trip jaringan tambahan untuk menampilkan kerangka visual gambar. Namun, pendekatan ini memiliki konsekuensi langsung pada ukuran respons HTTP.

Encoding biner ke representasi base64 menghasilkan peningkatan ukuran data secara deterministik sekitar 33%. Ketika string Data URI disematkan ke dalam JSON (contoh: data:image/webp;base64,...), ukuran respons keseluruhan membengkak. Masalah ini tereskalasi secara linear pada endpoint koleksi berpaginasi:

  • Respons daftar berisi 20 entri dengan LQIP 2 KB mentah menghasilkan ~53 KB beban base64 murni di luar data utama entitas.
  • Parser JSON di klien (terutama pada thread utama browser atau runtime seluler) memerlukan alokasi memori tambahan dan CPU time untuk memproses string berukuran masif tersebut.
  • Kompresi transfer (Gzip atau Brotli) memang menekan redundansi base64, tetapi dekompresi tetap menghasilkan raw byte yang membebani memori klien.

Batasan tegas harus ditetapkan pada kontrak API: LQIP inline maksimal berukuran 512 byte (setelah di-encode). Jika ukuran placeholder melebihi ambang batas ini, representasi harus dialihkan ke URL aset statis terpisah atau menggunakan representasi berbasis vektor mini (seperti SVG path dasar atau hash BlurHash/ThumbHash).

Spesifikasi Kontrak OpenAPI

Gunakan skema OpenAPI 3.0+ berikut untuk memberlakukan batasan panjang karakter pada atribut LQIP dan menegakkan pola Data URI yang valid.

components:
  schemas:
    MediaItem:
      type: object
      required:
        - id
        - url
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        lqip:
          type: string
          description: "Data URI base64 maksimal 512 byte"
          maxLength: 512
          pattern: '^data:image\/(webp|jpeg|png);base64,[A-Za-z0-9+/=]+$'
      example:
        id: "d3b07384-d113-4f90-87ef-489025816922"
        url: "https://cdn.example.com/images/sample.webp"
        lqip: "data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAADwAQCdASoFAAUAPm0ukUekIicjqAAA/v38gA=="

Strategi Cache-Control dan Edge CDN

Penyematan LQIP inline menyatukan siklus hidup cache data relasional dengan cache representasi gambar. Terapkan header respons berikut pada edge CDN:

  • Vary: Accept-Encoding, Accept: Mencegah collision cache antara respons terkompresi dan memastikan klien yang meminta format berbeda menerima respons yang tepat.
  • Cache-Control: Gunakan direktif public, max-age=60, s-maxage=300, stale-while-revalidate=60 untuk API data dinamis yang menyertakan thumbnail statis.
  • Jika placeholder disajikan via URL terpisah, gunakan public, max-age=31536000, immutable karena konten byte gambar placeholder bersifat permanen berbasis hash konten.

Implementasi Handler HTTP (Node.js)

Handler minimal berikut menggunakan Node.js core (modul node:http). Handler memvalidasi negosiasi konten, memastikan payload LQIP tidak melampaui batas 512 byte, serta menyematkan header keamanan dan caching.

// server.mjs
import http from 'node:http';

const MAX_LQIP_BYTES = 512;

const mockData = {
  id: 'd3b07384-d113-4f90-87ef-489025816922',
  url: 'https://cdn.example.com/images/prod-123.webp',
  // Valid WebP mini (1x1 pixel)
  lqip: 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4TC4AAAAvD8ADEA4QEQE1/kMREfkPBfVvbX8CQmQJkiQ1/VfSNAkCggA='
};

function validateLqipSize(dataUri) {
  const byteLength = Buffer.byteLength(dataUri, 'utf8');
  if (byteLength > MAX_LQIP_BYTES) {
    throw new Error(`LQIP payload size ${byteLength} exceeds limit of ${MAX_LQIP_BYTES} bytes`);
  }
}

const server = http.createServer((req, res) => {
  if (req.method !== 'GET' || req.url !== '/api/media') {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ error: 'Not Found' }));
  }

  const acceptHeader = req.headers['accept'];
  if (acceptHeader && !acceptHeader.includes('application/json') && !acceptHeader.includes('*/*')) {
    res.writeHead(406, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ error: 'Not Acceptable: Accept application/json' }));
  }

  try {
    validateLqipSize(mockData.lqip);
  } catch (err) {
    res.writeHead(500, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ error: 'Contract violation', details: err.message }));
  }

  const responseBody = JSON.stringify(mockData);

  res.writeHead(200, {
    'Content-Type': 'application/json; charset=utf-8',
    'Cache-Control': 'public, max-age=60, s-maxage=300',
    'Vary': 'Accept-Encoding, Accept',
    'X-Content-Type-Options': 'nosniff'
  });
  res.end(responseBody);
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

Pengujian Ukuran Payload Menggunakan Curl

Jalankan skrip shell berikut untuk memvalidasi negosiasi konten, status HTTP, dan batasan ukuran respons secara otomatis tanpa dependensi pustaka tambahan:

#!/usr/bin/env bash
set -euo pipefail

ENDPOINT="http://localhost:3000/api/media"
MAX_TOTAL_BYTES=1024

# 1. Validasi 406 Not Acceptable
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: text/plain" "$ENDPOINT")
if [ "$HTTP_STATUS" -ne 406 ]; then
  echo "FAIL: Ekspektasi HTTP 406, didapat $HTTP_STATUS"
  exit 1
fi

# 2. Ambil respons dan periksa ukuran byte
RESP_FILE=$(mktemp)
curl -s -w "%{http_code}" -H "Accept: application/json" "$ENDPOINT" -o "$RESP_FILE" > /dev/null

BYTES_COUNT=$(wc -c < "$RESP_FILE" | tr -d ' ')
rm -f "$RESP_FILE"

if [ "$BYTES_COUNT" -gt "$MAX_TOTAL_BYTES" ]; then
  echo "FAIL: Payload terlalu besar: $BYTES_COUNT bytes (maksimal $MAX_TOTAL_BYTES bytes)"
  exit 1
fi

echo "PASS: Response valid. Total payload: $BYTES_COUNT bytes."
exit 0