Web3 Development · 5 min read ·
Practical, high-impact gas optimization techniques for Solidity/EVM contracts, covering storage, calldata, control flow, and tooling.
Gas efficiency isn’t a “nice-to-have” in EVM development—it directly impacts user conversion, protocol competitiveness, and even security assumptions (think: block limits, MEV, and griefing). The best optimizations are rarely exotic assembly tricks; they’re usually about choosing the right data layout, avoiding unnecessary storage writes, and structuring execution so the common path is cheap.
Below are techniques we use in production audits and builds—prioritized by impact and reliability.
On EVM chains, the biggest recurring costs come from SSTORE/SLOAD. A single extra storage write in a hot function can dominate your gas profile.
Tactics that consistently pay off:
0 to a value is notably expensive; updating nonzero → nonzero is cheaper; clearing to zero may trigger refunds (subject to refund rules).Example pattern:
Be opinionated about “storage for convenience.” If something can be derived from existing state or verified from inputs, consider not storing it.
Calldata is cheaper than memory and vastly cheaper than storage. The classic win: prefer calldata in external functions.
external functions with calldata parameters for arrays/bytes/strings.calldata arrays into internal functions that expect memory will copy.A good practice is to mirror signatures:
function foo(uint256[] calldata xs) external and then have an internal helper that accepts uint256[] calldata as well.For frequently-called methods, this alone can shave meaningful gas.
The EVM stores state in 32-byte slots. If you store multiple small values, you want them to fit in one slot.
Good: pack uint128 + uint64 + uint64 in one slot.
Bad: scattering bool, uint8, uint16 across separate variables without packing considerations (Solidity packs only when variables are in the same storage “group” and order allows it).
Practical tips:
uint256 when packing isn’t possible anyway—smaller ints can introduce extra masking operations.This is a real-world lever in DeFi contracts with many per-position parameters.
A common gas mistake is choosing a data structure that fights your access pattern.
mapping(key => value) is typically best.Opinionated guidance: avoid unbounded loops over dynamic arrays in core functions. If you need enumeration, consider:
Events are cheaper than storage for logging, but they’re not free—especially with many indexed topics.
A pragmatic approach in protocols: keep events stable and index-friendly, but don’t “optimize away” critical audit trails. Saving 5k gas isn’t worth losing debuggability.
Custom errors (Solidity error) are cheaper than revert strings and improve ABI-level clarity.
require(x, "message") costs more due to string data.if (!x) revert MyError();This is an easy win across codebases with many checks.
Solidity 0.8+ inserts overflow checks by default. In tight loops or counter increments, unchecked can reduce overhead.
Use it only when you have a hard bound:
i++ where i < len.Don’t cargo-cult unchecked. If it weakens invariants, it’s not an optimization—it’s a bug.
Gas optimization is often about branch prediction by design:
Also consider:
a && b) to avoid evaluating expensive conditions unnecessarily.External calls cost gas and introduce risk (reentrancy, unexpected reverts). From a gas perspective:
Example: protocols that repeatedly query an oracle in multiple functions may benefit from fetching once per transaction and passing the price through (or using a cached value with explicit update semantics).
constant values are embedded in bytecode and are cheap to read.immutable values are set at deployment and read cheaply thereafter.Practical uses:
WETH, USDC, or a trusted router as immutable.constant/immutable.This reduces storage reads and simplifies reasoning.
Upgradeability adds runtime overhead:
DELEGATECALL and storage reads for implementation slots.If the contract is called constantly (e.g., an AMM pool), you should treat proxy overhead as part of your cost model. Sometimes the correct answer is:
A serious gas optimization loop looks like:
--optimize --optimizer-runs.Opinionated note: micro-optimizations that save <200 gas in a cold path are rarely worth readability loss. Optimize the top 5% of functions that drive 95% of spend.
The most reliable gas reductions come from state design: fewer writes, fewer reads, packed storage, and calldata-first APIs. After that, focus on control flow, revert strategy (custom errors), and avoiding unnecessary external calls. Finally, verify with tooling—because EVM gas costs are full of unintuitive tradeoffs, and “looks cheaper” isn’t evidence.
If you’re building a consumer-facing dapp or a high-frequency DeFi protocol, treat gas as a product feature. Users notice it instantly, and competitors exploit it ruthlessly.