Gas optimization artinya membuat contract butuh lebih sedikit gas untuk deploy dan untuk tiap panggilan, supaya pengguna membayar lebih murah. Bayangin ongkos di Toko Kaos Raka: menyewa rak gudang permanen itu mahal (itu storage), meja kerja yang dibereskan tiap malam murah (itu memory), dan nota yang dibawa pelanggan cuma dibaca, gak disimpan (itu calldata). Menghemat gas sebagian besar soal menyewa lebih sedikit rak. Dan aturan pentingnya: ukur dulu. Banyak tips gas di internet sudah basi atau gak berlaku untuk compiler sekarang.
🧪 Tebakan cepat: Raka mengganti
i++di loop-nya denganunchecked { ++i; }, seperti saran yang sering beredar. Dengan solc 0.8.37 dan optimizer menyala, berapa gas yang dihemat: sekitar 80 per putaran, sekitar 500 total, atau nol? Tebak dulu, jawabannya ada di bagian "unchecked: kapan aman, kapan gak ngaruh".
Cara mengukur
Semua angka di lesson ini diukur di chain lokal Hardhat 3.19.0 dengan solc 0.8.37: gasUsed dari receipt tiap transaksi, plus npx hardhat test --gas-stats. Ada dua profil build:
// hardhat.config.ts
solidity: {
profiles: {
default: { version: "0.8.37" },
production: { version: "0.8.37", settings: { optimizer: { enabled: true, runs: 200 } } },
},
},
npx hardhat test --build-profile production memakai optimizer. Penghematan pertama gak butuh mengubah kode sama sekali: deploy PoinRaka turun dari 1.229.456 jadi 687.557 gas (44% lebih murah). Angka di bawah memakai profil production, kecuali disebut lain.
Storage, memory, calldata
| Tempat | Hidup selama | Biaya (aturan EVM sekarang) |
|---|---|---|
| storage | selamanya, di semua node | baca pertama (cold) 2.100, berikutnya (warm) 100; tulis slot kosong 22.100, ubah slot 5.000, tulis ulang slot yang sama dalam transaksi yang sama 100 |
| memory | satu panggilan | beberapa gas per operasi |
| calldata | satu panggilan, read-only | beberapa gas per operasi, tanpa menyalin |
Raka mencatat 10 belanja Dinda di uint256[] belanja lalu menjumlahkannya ke total:
// Versi A: tulis storage tiap putaran
for (uint256 i = 0; i < belanja.length; i++) {
total += belanja[i];
}
// Versi B: jumlahkan di variabel lokal, tulis storage sekali
uint256 jumlah = 0;
uint256 n = belanja.length;
for (uint256 i = 0; i < n; i++) {
jumlah += belanja[i];
}
total = jumlah;
A = 72.604 gas, B = 69.487 gas: hemat 3.117, bukan "20.000 per putaran" seperti tips lama. Setelah disentuh sekali, slot total jadi warm dan tulis ulang cuma 100 gas. Untuk parameter array, calldata (47.220) lebih murah dari memory (48.205) karena gak menyalin; pakai calldata untuk parameter external yang gak diubah.
Struct packing
Storage dibagi slot 32 byte. Variabel kecil yang ditulis bersebelahan berbagi satu slot. Urutan field menentukan segalanya (layout dari solc storageLayout):
struct PelangganBoros { // 3 slot
uint128 poin; // slot 0, byte 0-15
uint256 id; // slot 1
uint128 belanja; // slot 2
}
struct PelangganRapat { // 2 slot
uint128 poin; // slot 0, byte 0-15
uint128 belanja; // slot 0, byte 16-31
uint256 id; // slot 1
}
Setelah daftar(Dinda, 1, 160 POIN, Rp 160.000), slot 0 versi rapat (dibaca dengan eth_getStorageAt) berisi dua nilai sekaligus: 0x00000000000000000000000000027100 0000000000000008ac7230489e800000. 16 byte kiri = 0x27100 = 160.000 (belanja), 16 byte kanan = 160 × 10^18 (poin). Pendaftaran baru: 89.043 jadi 66.903 gas (hemat 22.140, satu tulis slot baru). Mengubah data: 34.955 jadi 29.915.
Custom error vs require string
require(saldo >= amount, "Saldo POIN tidak cukup untuk ditukar"); // A
if (saldo < amount) revert NotEnoughPoin(saldo, amount); // B
require(saldo >= amount, NotEnoughPoin(saldo, amount)); // C, sejak solc 0.8.27
| A string | B custom error | C require + error | |
|---|---|---|---|
| bytecode runtime | 425 byte | 376 byte | 380 byte |
| gas deploy | 145.219 | 134.583 | 135.459 |
| gas transaksi yang revert | 23.801 | 23.765 | 23.768 |
Penghematan terbesarnya di ukuran contract dan biaya deploy; saat revert cuma 36 gas. Alasan utama memakai custom error justru lain: ia membawa data (saldo, amount) yang bisa dibaca viem.
Event vs storage
Mencatat riwayat belanja di array storage: 88.371 gas untuk catatan pertama. Memancarkan event Belanja(pelanggan, rupiah): 23.331. Hemat 65.040, karena event cuma masuk log receipt, bukan storage. Syaratnya: contract lain gak bisa membaca event. Pakai event untuk riwayat yang dibaca frontend atau indexer, storage untuk data yang dipakai logika contract.
immutable dan constant
address public immutable owner; // ditanam di bytecode saat deploy
uint256 public constant RUPIAH_PER_POIN = 1000; // ditanam saat compile
earn yang membaca owner dan rupiahPerPoin dari storage: 48.520 gas. Dengan immutable dan constant: 44.343, karena dua baca storage cold (2 × 2.100) hilang. Deploy-nya juga turun dari 206.645 ke 172.683. Ini penghematan tanpa biaya keterbacaan: pakai untuk nilai yang gak pernah berubah.
unchecked: kapan aman, kapan gak ngaruh
Sejak Solidity 0.8, + dan - dicek overflow. unchecked { } mematikan cek itu:
| Perubahan | Optimizer off | Optimizer on |
|---|---|---|
i++ jadi unchecked { ++i; } di loop 10 putaran |
70.727 jadi 70.677 | 69.487 jadi 69.487 |
saldo - amount setelah if (saldo < amount) revert |
27.096 jadi 26.917 | 26.744 jadi 26.677 |
Mengganti i++ di loop menghemat nol gas: sejak solc 0.8.22 compiler sendiri membuang cek overflow untuk penghitung loop sederhana. Itu jawaban tebakannya. Pengurangan setelah pengecekan menghemat 67 gas dan aman, karena baris sebelumnya membuktikan saldo >= amount; OpenZeppelin ERC20 melakukan persis itu. Tanpa pembuktian seperti itu, unchecked membuat bug overflow diam-diam.
🤔 Kapan optimasi gak sepadan
Urutan yang masuk akal: nyalakan optimizer, kurangi tulis storage (event, variabel lokal), pack struct yang sering ditulis, immutable/constant. Setelah itu, tiap trik harus dibandingkan dengan biayanya: kode yang lebih sulit dibaca lebih sulit diaudit, dan bug di contract jauh lebih mahal dari 67 gas. Inline assembly atau trik bitwise untuk fungsi admin yang dipanggil sebulan sekali hampir gak pernah sepadan.
⚠️ Jebakan yang sering ketemu
- Percaya angka tanpa mengukur. Tabel gas lama (refund 15.000, "tiap SSTORE 20.000") sudah basi: upgrade Istanbul, Berlin dan London mengubah biaya storage. Ukur dengan
--gas-statsdan receipt. - Urutan field yang memecah slot.
uint128, uint256, uint128makan 3 slot;uint128, uint128, uint256cuma 2. uncheckedtanpa bukti. Aman cuma kalau baris sebelumnya membuktikan gak ada overflow.- Mengganti storage dengan event untuk data yang dibaca contract. Contract gak bisa membaca event; logikanya rusak.
TL;DR: Ukur, jangan menebak: npx hardhat test --gas-stats dan gasUsed di receipt, dengan optimizer (runs: 200) yang sendirian memotong deploy PoinRaka dari 1.229.456 ke 687.557. Penghematan terbesar datang dari mengurangi SSTORE: event alih-alih storage (88.371 jadi 23.331), variable packing yang menaruh field kecil bersebelahan dalam slot 32 byte (89.043 jadi 66.903), dan menulis storage sekali di akhir loop (hemat 3.117, karena slot warm cuma 100 gas). calldata lebih murah dari memory untuk parameter, immutable/constant menghapus baca storage (hemat 4.177), custom error memperkecil bytecode (425 jadi 376 byte) dan membawa data. unchecked { ++i; } menghemat nol gas sejak solc 0.8.22; unchecked cuma layak setelah pengecekan yang membuktikan aman. Kalau sebuah trik merusak keterbacaan demi puluhan gas, lewati.