Menghemat biaya di setiap transaksi: pola gas optimization, penggunaan calldata, storage layout yang efisien, immutable dan constant, serta praktik mengoptimasi kontrak sampai gas report turun signifikan

Setelah di episode 16 kalian membangun lapisan data, pada episode ini kita bicara uang: gas optimization. Di Web2, optimasi berarti server yang lebih cepat; di Web3, setiap operasi on-chain punya harga tetap, dan kalian (atau pengguna kalian) membayarnya. Kontrak yang boros gas bukan sekadar "kurang efisien" — ia kehilangan pengguna karena setiap interaksi terasa mahal.
Episode ini membahas pola-pola yang paling berdampak: storage layout, calldata, immutable, dan teknik yang menurunkan gas report secara nyata.
Dalam EVM, biaya penyimpanan data di storage jauh lebih mahal daripada komputasi. Operasi SSTORE (menulis storage) memakan puluhan ribu gas, sementara komputasi matematika murah. Karena itu, hampir semua optimasi gas berpusat pada satu pertanyaan: bagaimana meminimalkan akses storage?
Prinsip dasar biaya gas:
SSTORE (set baru) → ~20.000 gas
SLOAD (baca) → ~100 gas (panas), ~2.100 (dingin)
SSTORE (set 0) → diskon, tapi jarang dimanfaatkan
Komputasi sederhana → puluhan gasSatu operasi storage bisa setara dengan ribuan operasi komputasi. Optimasi terbaik sering kali berarti tidak menyentuh storage sama sekali.
EVM menyimpan state dalam slot 32 byte. Beberapa variabel kecil dalam satu slot menghemat biaya:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Packed {
// Buruk: 3 slot terpisah → 3x biaya storage
uint128 public a1;
uint128 public b1;
uint128 public c1;
// Baik: 3 variabel mengisi 1 slot (3 * 16 = 48 byte ≤ 32? tidak!)
// uint128 * 3 = 48 byte > 32 byte → tetap 2 slot
uint64 public a2;
uint64 public b2;
uint64 public c2;
uint64 public d2;
}Aturan praktis packing:
uint128 + uint128 → 1 slot (32 byte pas).uint64 x 4 → 1 slot.address (20 byte) + uint96 → 1 slot — pola paling umum di token (misal storage balance).Contoh packing yang benar dan sering dipakai:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract VaultStorage {
// 1 slot: address (20) + uint96 (12) = 32 byte
struct Position {
address token;
uint96 amount;
}
// Bad: dua field terpisah = 2 slot
mapping(address => uint256) public badBalances;
// Good: satu struct = 1 slot per mapping entry
mapping(address => Position) public positions;
}Setiap slot yang bisa dihemat berarti puluhan ribu gas per transaksi — di volume jutaan transaksi, ini perbedaan jutaan dolar.
Parameter fungsi adalah area optimasi termudah. Mengubah memory menjadi calldata menghilangkan biaya penyalinan ke memory:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Calldata {
// Buruk: menyalin ke memory, lebih mahal
function processMemory(string memory _data) external pure {
// ...
}
// Baik: baca langsung dari calldata, murah
function processCalldata(string calldata _data) external pure {
// ...
}
}Aturan: untuk parameter yang hanya dibaca (tidak dimodifikasi, tidak di-assign), gunakan calldata. Untuk data yang akan dimodifikasi, gunakan memory.
Nilai yang ditentukan sekali di constructor dan tidak pernah berubah sebaiknya disimpan sebagai immutable, bukan variabel storage biasa — immutable disematkan langsung di bytecode kontrak, bukan disimpan di storage:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Immutable {
// Buruk: SLOAD untuk membaca di setiap transaksi
address public owner;
uint256 public fee = 100;
// Baik: nilai tertanam di bytecode, tanpa SLOAD
address public immutable deployer;
uint256 public constant FEE = 100;
constructor() {
deployer = msg.sender;
owner = msg.sender;
}
function isDeployer() external view returns (bool) {
// immutable: tidak ada biaya baca storage
return msg.sender == deployer;
}
}constant (nilai ditentukan saat compile) dan immutable (ditentukan saat deploy) menghilangkan biaya SLOAD setiap kali dibaca. Untuk nilai seperti fee, denominator, dan alamat protocol, ini optimasi besar dengan usaha nol.
Tip
Gunakan forge test --gas-report atau plugin hardhat-gas-reporter sejak awal, bukan setelah kontrak selesai. Membaca gas report setiap kali menulis fungsi menanamkan kebiasaan optimasi yang sulit didapat lewat teori.
Beberapa pola tambahan yang sering muncul di audit:
| Pola | Dampak |
|---|---|
unchecked block (Solidity 0.8+) | Menghindari cek overflow — hanya jika aman secara matematis |
| Meminimalkan event data | Kurangi non-indexed params |
| Merkle root untuk allowlist | Satu slot, bukan list panjang |
| Batch operations | Gabungkan banyak transfer dalam satu transaksi |
| Cache SLOAD dalam loop | Simpan nilai storage ke variabel lokal sekali |
Contoh loop yang meng-cache storage:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract LoopOptimization {
uint256 public total;
// Buruk: membaca storage di setiap iterasi (N x SLOAD)
function badLoop(uint256[] calldata _items) external {
for (uint256 i = 0; i < _items.length; i++) {
total += _items[i];
}
}
// Baik: baca sekali, tulis sekali
function goodLoop(uint256[] calldata _items) external {
uint256 cached = total;
for (uint256 i = 0; i < _items.length; i++) {
cached += _items[i];
}
total = cached;
}
}Dengan meng-cache total ke variabel lokal, N kali SLOAD + SSTORE berubah menjadi 1 kali SLOAD + 1 kali SSTORE. Untuk 100 item, ini menghemat ratusan ribu gas.
Warning
Optimasi gas tidak boleh mengorbankan keamanan. unchecked yang menonaktifkan cek overflow hanya aman jika kalian sudah membuktikan nilai tidak mungkin melampaui batas — salah memakai unchecked adalah sumber bug overflow klasik di episode 18. Optimasi setelah fungsi terbukti benar; bukan sebelum.
Latihan praktis:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract BeforeAfter {
mapping(address => uint256) public balanceOf;
// SEBELUM optimasi
function deposit(uint256 _amount) external {
require(_amount > 0, "zero");
balanceOf[msg.sender] += _amount;
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract After {
// Optimasi: mapping + struct packing
struct Entry {
uint128 balance;
uint128 lastUpdate;
}
mapping(address => Entry) public entries;
// SESUDAH: satu slot per entry, biaya storage lebih kecil
function deposit(uint128 _amount) external {
require(_amount > 0, "zero");
Entry storage e = entries[msg.sender];
e.balance += _amount;
e.lastUpdate = uint128(block.timestamp);
}
}Jalankan forge test --gas-report pada kedua versi dan bandingkan angka deployment dan fungsi — kalian akan melihat perbedaan nyata. Itulah angka yang kalian bawa ke review code.
Inti yang harus dibawa pulang:
SSTORE/SLOAD.calldata untuk parameter read-only, immutable/constant untuk nilai tetap.Di episode 18 selanjutnya kita masuk ranah paling serius — Smart Contract Security: reentrancy, overflow, front-running, dan pola CEI, dengan praktik security review. Mari lanjut ke episode 18!