Web3 Development · 5 min read ·
Practical gas optimization techniques for Solidity/EVM contracts: storage patterns, calldata, packing, loops, libraries, and deployment tradeoffs.
Gas costs aren’t just a UX tax—they’re a product constraint. High gas makes your protocol harder to use, raises liquidation thresholds, kills small-ticket users, and turns “nice-to-have” safety checks into a constant internal debate. The good news: most meaningful savings come from a handful of patterns that are easy to institutionalize in your codebase.
This article focuses on optimization techniques that reliably move the needle on EVM chains (Ethereum L1 and EVM L2s), without resorting to unreadable hacks.
Before touching code, get repeatable benchmarks.
forge test --gas-report and forge snapshot to track regressions in CI.swap, mint, withdraw, liquidate, claim, or tight loops in accounting.Opinionated take: if you’re not pinning gas snapshots in CI for core functions, you’re not “optimizing”—you’re guessing.
On the EVM, storage dominates cost. A single SSTORE can dwarf multiple arithmetic operations.
Repeatedly reading the same storage slot costs multiple SLOADs.
A write that doesn’t change the value can still cost gas (and complicate refunds).
For reward distribution, a global “push to all users” approach forces loops and writes. “Pull” designs store per-user checkpoints and compute owed rewards on demand, reducing storage churn.
Solidity packs values into 32-byte slots when types fit.
uint96, uint128, uint32).Example: a struct like
uint128 balance; uint64 lastUpdated; uint64 flags;
can fit into one slot instead of three.Tradeoff: packed fields can cost extra ops when extracting and updating, especially if you update only one packed field frequently (you may incur read-modify-write). Pack fields that tend to be updated together.
calldata for external function parametersFor external functions, prefer calldata over memory for arrays/bytes/strings:
function foo(bytes calldata data) external is cheaper than copying into memory.immutable and constantconstant is embedded in bytecode.immutable is set once in the constructor and accessed cheaply.Typical targets: addresses (router, token, oracle), scalar params (fee bps, precision).
Revert strings increase bytecode size and runtime costs.
error Unauthorized(); and revert Unauthorized();For a protocol with many require statements, this is one of the easiest “free wins.”
Loops scale gas linearly—and can DOS your function if inputs grow.
If you must loop:
uint256 n = arr.length;unchecked { ++i; } when safe.unchecked arithmetic, but only after invariantsSolidity 0.8+ adds overflow checks. These are good—but sometimes redundant.
Use unchecked blocks where:
i < n).Do not blanket-disable checks in financial logic unless you can demonstrate invariants in tests and audits. Gas savings aren’t worth subtle overflow risk.
bytes32 keys and avoid heavy hashing patternsMappings are efficient, but key choice matters.
bytes32, address, uint256) over dynamic keys.Example: rather than repeatedly computing keccak256(abi.encodePacked(a,b,c)) in multiple places, compute it once and pass around the bytes32 id.
abi.encodePacked footgunsFrom a gas perspective, abi.encodePacked can be cheaper, but it’s risky if you’re building hashes with ambiguous concatenation.
abi.encode for unambiguous hashing.encodePacked, ensure types and boundaries prevent collisions.Optimization is pointless if it introduces a signature collision or ID ambiguity.
Every external call adds overhead and risk.
Techniques:
Be careful: caching oracle data can create stale-price risk. Gas savings must not weaken economic security.
Inlining can reduce call overhead but increase bytecode size. On mainnet, deployment gas matters; on L2, runtime might matter more.
Practical guidance:
internal functions.Also consider splitting large systems into modules (diamond/proxy patterns) carefully—delegation adds overhead on every call.
Events are cheaper than storage for historical data, but they still cost gas.
A good pattern is emitting compact identifiers (like bytes32 positionId) and letting off-chain systems resolve metadata.
Effective EVM gas optimization is mostly about three things: minimizing storage writes, keeping hot paths tight, and avoiding accidental copies/overhead. The best teams treat gas like performance engineering: measure, change one variable at a time, lock gains with snapshots, and never trade away correctness for a few thousand gas.
If you’re building production DeFi or high-frequency on-chain systems, build a culture of gas reviews: every new feature should answer “what does this cost per call, and who pays it?” That mindset will save you more than any single micro-optimization.