Solidity Gas Optimization: 25 Best Must-Have Tips

Gas costs shape both user fees and protocol economics. Small micro-optimizations add up across hot paths like mints, swaps, and tight loops. Aim for clear code first, then apply targeted tweaks where data proves they help.
The tips below focus on patterns that save gas without hurting readability. Each one has a quick reason and a tiny example so you can apply it with confidence.
How gas costs add up
Storage writes cost the most, memory is cheaper, and stack ops are cheapest. External calls are expensive, and loops multiply every mistake. A single SSTORE can dwarf dozens of arithmetic ops. Measure before and after to confirm real wins.
Picture a simple NFT mint loop. If you store every interim value to storage, you pay repeatedly. If you buffer in memory and commit once, you cut costs while keeping intent clear.
Fast wins most teams miss
These quick checks catch common waste in contracts that already work. They are safe and simple to adopt in most codebases.
- Use
calldatafor read-only function params. - Short-circuit checks early to avoid heavy work.
- Cache global reads like
msg.senderinto locals. - Pack small storage variables to reduce slots.
- Replace
stringwithbytesfor raw data ops.
Apply these in hot paths first. You often recover 5–20% gas without any deep refactor.
25 best must-have tips
Use this checklist during reviews. Each tip notes why it saves gas and shows a tiny pattern.
-
Prefer
calldatafor external read-only params.Reason: avoids memory copy.
Example:
function f(address a, uint256[] calldata xs) external {} -
Mark variables
immutablewhen set in constructor.Reason: cheaper than storage reads.
Example:
address immutable OWNER; -
Pack storage slots.
Reason: fewer
SSTOREoperations.Example:
uint128 a; uint128 b;fits in one slot. -
Use
uncheckedfor arithmetic where overflow cannot occur.Reason: skips SafeMath checks.
Example:
unchecked { i += 1; } -
Pre-increment in loops (
++i).Reason: slightly cheaper than
i++.Example:
for (uint256 i; i < n; ++i) {} -
Cache storage reads into memory.
Reason: storage read costs repeat.
Example:
uint x = s.value; if (x > 0) { use(x); } -
Write storage once after a loop.
Reason: batch updates.
Example: accumulate in memory, then
SSTOREonce. -
Use
bytes32oruint256over smaller ints when not packing.Reason: fewer masking ops.
Example: use
uint256counters. -
Use
constantfor static values.Reason: inlines at compile time.
Example:
uint256 constant WAD = 1e18; -
Avoid redundant zero-initialization.
Reason: default value is zero.
Example:
uint256 x;notuint256 x = 0; -
Short-circuit
requireearly.Reason: stops costly work.
Example: check balance before reading arrays.
-
Use
calldataslicing with care.Reason: avoid memory copies for ABI-encoded data.
Example: parse selectors from
msg.datawhen safe. -
Replace
stringcompares withbytes32tags.Reason: strings cost more to hash/compare.
Example:
bytes32 constant TAG = keccak256("FOO"); -
Emit minimal event data.
Reason: logs cost per byte and per topic.
Example: index only what you query.
-
Use custom errors instead of
revert("message").Reason: shorter revert data.
Example:
error NotOwner(); -
Prefer
internaloverpublicfor library-like helpers.Reason: enables inlining.
Example:
function _calc(...) internal returns (...) -
Avoid
abi.encodePackedwhenabi.encodefits.Reason: packed encoding can add keccak and padding work.
Example: store structured data with
encode. -
Use
deleteto reclaim storage gas refund when reset to zero.Reason: refunds reduce total gas.
Example:
delete user.allowance; -
Prefer
mappingoverarrayscans.Reason: O(1) access beats loops.
Example:
mapping(address => bool) isMember; -
Move constants to compile-time expressions.
Reason: avoids runtime math.
Example: precompute rates and scale factors.
-
Use
payableonly when needed.Reason: small savings across calls.
Example: non-payable for view-like writes.
-
Consolidate modifiers or inline where simple.
Reason: reduces extra jumps.
Example: one
onlyOwnercheck inside function. -
Avoid duplicate hashing.
Reason: keccak256 is costly.
Example: compute once, reuse the digest.
-
Use
assemblyonly for proven wins in hot spots.Reason: removes overhead, but needs care.
Example: tight
memcopy, optimizedkeccakpreimage. -
Use
boolsparingly in storage unless packed.Reason: can cost full slot; pack into
uint256bit flags.Example:
uint256 flags; /set bits for states
Do not force all 25 into every function. Pick the top five that apply to your hot path, measure, and iterate. Clean code still matters for audits and long-term upkeep.
Micro-examples that save real gas
These small snippets show how a minor change shifts costs in loops and storage-heavy paths.
/1) Cache storage in a loop
for (uint256 i; i < users.length; ++i) {
address u = users[i]; /storage read once
balances[u] += reward; /one SLOAD + one SSTORE per loop
}
/2) Pack two fields
struct Position { uint128 size; uint128 cost; } /fits in one slot
/3) Use custom error
if (msg.sender != owner) revert NotOwner();
Run differential tests on these blocks with a gas reporter. You can see gains from 3–30% depending on how often the path runs.
Common patterns and typical savings
Use this table to spot easy refactors with clear payoffs. Savings vary by compiler and context, but the direction holds steady.
| Pattern | Why it saves | Typical impact |
|---|---|---|
| Move params to calldata | Avoids memory copy | 1–5% per call |
| Pack storage fields | Fewer storage slots | 10–30% in structs |
| Batch storage writes | Cut repeated SSTORE | 5–25% in loops |
| Custom errors | Short revert data | 200–500 gas on fail |
| Bit flags over bools | Packs many flags | Large in state-heavy logic |
Confirm on your codebase with your compiler version. A small structural shift can outdo many micro-tweaks combined.
Testing and measuring gas
Guessing wastes time. Use a repeatable method to prove savings and avoid regressions across releases.
- Add a gas reporter to your test stack (Hardhat or Foundry).
- Write targeted tests that hit hot paths with realistic inputs.
- Record baselines before changes and commit the numbers.
- Apply one change at a time; re-run benchmarks.
- Review trade-offs with readability and security in mind.
Keep a changelog of gas diffs. This audit trail speeds reviews and supports future upgrades when the team changes.
Safety notes while optimizing
Gas wins must not weaken safety. Some shortcuts hide traps. Watch for these during reviews and audits.
- Only use
uncheckedwhere overflow is provably impossible. - Do not skip input checks that prevent reentrancy or logic bugs.
- Be careful with
assembly; missing bounds checks invite exploits. - Keep events sufficient for off-chain indexing needs.
Pair each optimization with tests and a clear comment. Future you will thank present you during incident response.
Putting it into practice
Start with the simplest wins: calldata params, storage packing, custom errors, and caching reads. Then move to loop batching and bit flags. Leave assembly for last, and only inside bottlenecks you can prove.
A team that measures, documents, and ships small gas improvements each sprint will see fees drop and throughput rise. Users will notice the difference at mint time and during peak activity.


