Penambahan library pihak ketiga sering kali meloloskan lonjakan ukuran aplikasi tanpa disadari oleh reviewer PR. Review manual terhadap package.json atau lockfile tidak dapat memprediksi dampak nyata tree-shaking, kode polifil, atau dependensi transitif terhadap artifak final. Solusi deterministik untuk masalah ini adalah mengukur ukuran bundle JavaScript dan bytecode Hermes secara langsung di pipeline CI, lalu menampilkan perbandingannya ke PR menggunakan Danger JS.
Arsitektur Pengukuran Bundle
Pemeriksaan diff ukuran bundle bekerja dengan mengompilasi bundle pada dua titik git yang berbeda: target branch (misalnya main) dan PR branch. Output kompilasi dianalisis untuk menghasilkan metrik ukuran byte sebelum dan sesudah kompresi.
- Raw JS Bundle: File JavaScript hasil packaging Metro bundler.
- Hermes Bytecode (HBC): Format biner yang dieksekusi oleh Hermes engine pada runtime Android/iOS. Metrik ini merepresentasikan ukuran riil yang masuk ke APK/AAB atau IPA.
- Assets: Folder gambar dan font yang diproses selama proses bundling.
- Gzipped Size: Estimasi kompresi over-the-wire untuk Over-The-Air (OTA) update seperti CodePush.
1. Skrip Ekstraksi Ukuran Bundle (Node.js)
Skrip berikut mengeksekusi Metro bundler, mengompilasi bundle ke bytecode Hermes menggunakan biner hermesc bawaan React Native, menghitung ukuran byte file, dan menyimpan hasilnya dalam format JSON.
// scripts/measure-bundle.js
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const OUT_DIR = path.resolve(process.argv[2] || './build-output');
const REPORT_FILE = path.resolve(process.argv[3] || './bundle-report.json');
fs.mkdirSync(OUT_DIR, { recursive: true });
const bundlePath = path.join(OUT_DIR, 'index.android.bundle');
const hbcPath = path.join(OUT_DIR, 'index.android.hbc');
const assetsPath = path.join(OUT_DIR, 'assets');
// 1. Eksekusi Metro Bundler
execSync(
`npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output ${bundlePath} --assets-dest ${assetsPath}`,
{ stdio: 'inherit' }
);
// 2. Kompilasi ke Hermes Bytecode
const hermescBin = path.resolve(
'node_modules/react-native/sdks/hermesc/%OS_BIN%/hermesc'
.replace('%OS_BIN%', process.platform === 'darwin' ? 'osx-bin' : 'linux64-bin')
);
execSync(`${hermescBin} -emit-binary -out ${hbcPath} ${bundlePath}`);
// Helper penghitung ukuran direktori asset
function getDirSize(dir) {
if (!fs.existsSync(dir)) return 0;
return fs.readdirSync(dir, { withFileTypes: true }).reduce((total, dirent) => {
const res = path.resolve(dir, dirent.name);
return total + (dirent.isDirectory() ? getDirSize(res) : fs.statSync(res).size);
}, 0);
}
const jsBuffer = fs.readFileSync(bundlePath);
const hbcBuffer = fs.readFileSync(hbcPath);
const metrics = {
rawJsBytes: jsBuffer.length,
rawJsGzipBytes: zlib.gzipSync(jsBuffer).length,
hermesBytecodeBytes: hbcBuffer.length,
hermesGzipBytes: zlib.gzipSync(hbcBuffer).length,
assetsBytes: getDirSize(assetsPath),
timestamp: new Date().toISOString(),
};
fs.writeFileSync(REPORT_FILE, JSON.stringify(metrics, null, 2));
console.log(`Laporan ukuran bundle tersimpan di: ${REPORT_FILE}`);
2. Konfigurasi Dangerfile untuk Diff & Budget Threshold
Danger JS membaca hasil JSON dari branch base dan branch PR, mengalkulasi selisih (delta), memformat tabel Markdown, lalu memberi warning atau memblokir merge jika threshold regresi terlampaui.
// dangerfile.ts
import { danger, fail, warn, markdown } from 'danger';
import * as fs from 'fs';
import * as path from 'path';
interface BundleMetrics {
rawJsBytes: number;
rawJsGzipBytes: number;
hermesBytecodeBytes: number;
hermesGzipBytes: number;
assetsBytes: number;
}
const baseReportPath = path.resolve('./base-bundle-report.json');
const prReportPath = path.resolve('./pr-bundle-report.json');
if (!fs.existsSync(baseReportPath) || !fs.existsSync(prReportPath)) {
warn('Data komparasi bundle size tidak lengkap. Pengecekan diff dilewati.');
} else {
const base: BundleMetrics = JSON.parse(fs.readFileSync(baseReportPath, 'utf-8'));
const pr: BundleMetrics = JSON.parse(fs.readFileSync(prReportPath, 'utf-8'));
// Thresholds (dalam Bytes)
const WARN_THRESHOLD = 50 * 1024; // 50 KB
const FAIL_THRESHOLD = 150 * 1024; // 150 KB
const formatBytes = (bytes: number) => {
const sign = bytes > 0 ? '+' : '';
return `${sign}${(bytes / 1024).toFixed(2)} KB`;
};
const hbcDiff = pr.hermesBytecodeBytes - base.hermesBytecodeBytes;
const hbcGzipDiff = pr.hermesGzipBytes - base.hermesGzipBytes;
const assetDiff = pr.assetsBytes - base.assetsBytes;
const totalDiff = (pr.hermesBytecodeBytes + pr.assetsBytes) - (base.hermesBytecodeBytes + base.assetsBytes);
let statusIcon = '✅';
if (totalDiff > FAIL_THRESHOLD) statusIcon = '🚨';
else if (totalDiff > WARN_THRESHOLD) statusIcon = '⚠️';
let table = `### ${statusIcon} Analisis Ukuran Bundle React Native (Android)
`;
table += '| Target | Base Branch | PR Branch | Diff (Bytes) |
';
table += '| :--- | :--- | :--- | :--- |
';
table += `| **Hermes Bytecode** | ${(base.hermesBytecodeBytes / 1024).toFixed(2)} KB | ${(pr.hermesBytecodeBytes / 1024).toFixed(2)} KB | ${formatBytes(hbcDiff)} |
`;
table += `| **Hermes (Gzip)** | ${(base.hermesGzipBytes / 1024).toFixed(2)} KB | ${(pr.hermesGzipBytes / 1024).toFixed(2)} KB | ${formatBytes(hbcGzipDiff)} |
`;
table += `| **Assets** | ${(base.assetsBytes / 1024).toFixed(2)} KB | ${(pr.assetsBytes / 1024).toFixed(2)} KB | ${formatBytes(assetDiff)} |
`;
table += `| **Total Distribution** | ${((base.hermesBytecodeBytes + base.assetsBytes) / 1024).toFixed(2)} KB | ${((pr.hermesBytecodeBytes + pr.assetsBytes) / 1024).toFixed(2)} KB | **${formatBytes(totalDiff)}** |
`;
markdown(table);
if (totalDiff > FAIL_THRESHOLD) {
fail(`Ukuran bundle membengkak sebesar ${formatBytes(totalDiff)}. Batas maksimal adalah ${FAIL_THRESHOLD / 1024} KB.`);
} else if (totalDiff > WARN_THRESHOLD) {
warn(`Peringatan: Regresi bundle size mendekati limit (${formatBytes(totalDiff)}). Pastikan dependensi baru diimpor secara efisien.`);
}
}
3. Otomasi Pipeline via GitHub Actions
Workflow berikut menjalankan kompilasi dua kali: pertama pada branch tujuan PR (base), kemudian pada commit PR saat ini (head). Data kedua kompilasi dibandingkan langsung sebelum memicu Danger.
# .github/workflows/bundle-size-diff.yml
name: Bundle Size Diff
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'src/**'
- 'package.json'
- 'yarn.lock'
- 'index.js'
jobs:
analyze-bundle:
runs-on: ubuntu-latest
steps:
- name: Checkout PR Branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'yarn'
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Measure PR Branch
run: node scripts/measure-bundle.js ./build-pr ./pr-bundle-report.json
- name: Checkout Base Branch
run: |
git checkout ${{ github.base_ref }}
yarn install --frozen-lockfile
- name: Measure Base Branch
run: node scripts/measure-bundle.js ./build-base ./base-bundle-report.json
- name: Checkout back to PR Branch (for Danger context)
run: git checkout ${{ github.event.pull_request.head.ref }}
- name: Run Danger JS
run: npx danger ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Optimasi dan Mitigasi Trade-off
1. Durasi Build CI
Mengompilasi dua branch dalam satu job menambah waktu eksekusi CI (rata-rata 1-3 menit per build). Untuk mempercepat runtime:
- Simpan metrik bundle branch
mainke dalam GitHub Actions Cache atau AWS S3 setiap kali PR di-merge ke main. Job PR hanya perlu mengompilasi head branch dan mengambil artifak JSON base dari cache. - Gunakan path filter di GitHub Actions agar job hanya berjalan jika terjadi perubahan file JS/TS, aset, atau dependensi manifest.
2. Hermes Bytecode Determinism
Bytecode Hermes menyertakan metadata dependensi. Jika terdapat perubahan minor pada path file atau compiler version, ukuran bytecode dapat bergeser beberapa byte meski logika kode tidak berubah. Tentukan threshold toleransi minimal (misal: 1–5 KB) untuk mengabaikan noise fluktuasi compiler.
3. Platform Differences
Hasil kompilasi bytecode Android dan iOS memiliki struktur biner yang identik jika target Hermes versi sama, namun struktur asset bisa berbeda jika ada konfigurasi density-specific (drawable-hdpi vs @2x/@3x). Jika aplikasi Anda bergantung intensif pada native asset catalog iOS, jalankan komparasi terpisah untuk platform ios.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!