Web3 Development · 5 min read ·
Practical gas optimization techniques for Solidity and EVM contracts, from storage layout and calldata to packing, caching, and testing tradeoffs.
Gas is the real “cloud bill” of Web3. If you ship a contract that’s 20–40% more expensive than it needs to be, you’re effectively taxing every user interaction forever. The good news: most meaningful savings come from a handful of repeatable patterns—storage discipline, calldata usage, and avoiding unnecessary work.
This post focuses on techniques that reliably move the needle without turning your codebase into an unreadable puzzle.
Before changing code, identify which functions and op patterns burn gas.
forge test --gas-report gives per-function gas.hardhat-gas-reporter for CI comparisons.Optimization is contextual: saving 5,000 gas on a function called once per deployment is pointless; saving 2,000 gas on a hot path (swap, mint, claim) is real money.
Accessing storage (SLOAD) is far more expensive than reading memory or calldata. Common pattern:
SSTORE at the end.Example: cache a mapping read:
balances[msg.sender].uint256 bal = balances[msg.sender]; then operate on bal, then write back once.Solidity packs smaller types into a single 32-byte slot when declared adjacently. If you use uint256 everywhere, you waste storage.
uint128, uint64, uint32, bool (careful), small enums.Example: Instead of uint256 amount; uint256 timestamp; bool claimed;, consider uint128 amount; uint64 timestamp; bool claimed; (and order them for packing).
boolbool looks small but can create extra reads/writes depending on packing and how you update it. If a flag sits alone in a slot, it’s wasteful; if it packs cleanly with other fields, it can be fine.
push) and storing long strings/bytes are expensive.bytes32 identifiers instead of strings.For external functions, prefer calldata over memory for read-only parameters:
function foo(bytes calldata data) external is cheaper than bytes memory data because calldata avoids copying.uint256[] calldata amounts.Also: if you accept signatures, permit payloads, or proofs, treat them as calldata and decode minimally.
Revert strings are expensive: they add deployment bytecode and increase runtime costs when reverting.
error Unauthorized();revert Unauthorized();This reduces bytecode size and is now standard for production Solidity.
External calls are not just risky; they’re expensive.
claimMany() or mintBatch() when possible.IERC20 token = IERC20(_token); if reused.A practical example from DeFi routers: validate array lengths and deadlines first (cheap), then do transfers/swaps.
Loops are common gas sinks.
unchecked { ++i; } in for loops when overflow is impossible (typical for index increments).uint256 len = arr.length; then loop to len.Be honest: if users can pass unbounded arrays, you’re also creating DoS risk via block gas limits. Put reasonable caps.
Mappings are usually cheaper than arrays for lookups.
mapping(address => bool) beats scanning an array.If you must enumerate on-chain, maintain both:
mapping(key => index) plus an array of keys (classic “index mapping” pattern), but note the extra writes.constant values are embedded in bytecode.immutable values are set in the constructor and then read cheaply.Use these for addresses like routers, token addresses, or domain separators that don’t change. Avoid storing such values in regular state unless you need upgradeability.
Hashing is common in permits, Merkle claims, and EIP-712.
keccak256(abi.encodePacked(...)) only when safe from collisions; otherwise use abi.encode.Example: Airdrop claims can be significantly cheaper by hashing leaf nodes as keccak256(bytes.concat(keccak256(abi.encode(account, amount)))) vs. heavier encodings—though correctness beats micro-savings.
Assembly can reduce overhead for:
But assembly also increases audit surface area. If the savings are <1–2% on a non-hot path, it’s usually not worth it.
A balanced approach: rely on battle-tested libraries (e.g., Solady, OpenZeppelin where appropriate) and confine assembly to small, well-tested modules.
Large bytecode increases deployment gas and can push you near the 24KB contract size limit.
Tactics:
The biggest gas wins often come from changing the flow:
Example: Many reward distribution systems moved from “store each user’s claimable balance” to “store a global reward-per-token index,” cutting writes dramatically.
Gas optimization isn’t about clever tricks—it’s about disciplined state management and avoiding unnecessary work. If you focus on (1) fewer storage writes, (2) calldata-friendly interfaces, (3) tight loops and batching, and (4) smaller revert/bytecode footprint, you’ll capture most of the savings that matter in production.
The final rule: don’t optimize blindly. Measure, change one thing at a time, and keep code auditable. Cheap contracts are great—but cheap, correct, and maintainable contracts are what win.