Solidity Gas Optimization: 25 Best Must-Have Tips

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 calldata for read-only function params.
  • Short-circuit checks early to avoid heavy work.
  • Cache global reads like msg.sender into locals.
  • Pack small storage variables to reduce slots.
  • Replace string with bytes for 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.

  1. Prefer calldata for external read-only params.

    Reason: avoids memory copy.

    Example: function f(address a, uint256[] calldata xs) external {}

  2. Mark variables immutable when set in constructor.

    Reason: cheaper than storage reads.

    Example: address immutable OWNER;

  3. Pack storage slots.

    Reason: fewer SSTORE operations.

    Example: uint128 a; uint128 b; fits in one slot.

  4. Use unchecked for arithmetic where overflow cannot occur.

    Reason: skips SafeMath checks.

    Example: unchecked { i += 1; }

  5. Pre-increment in loops (++i).

    Reason: slightly cheaper than i++.

    Example: for (uint256 i; i < n; ++i) {}

  6. Cache storage reads into memory.

    Reason: storage read costs repeat.

    Example: uint x = s.value; if (x > 0) { use(x); }

  7. Write storage once after a loop.

    Reason: batch updates.

    Example: accumulate in memory, then SSTORE once.

  8. Use bytes32 or uint256 over smaller ints when not packing.

    Reason: fewer masking ops.

    Example: use uint256 counters.

  9. Use constant for static values.

    Reason: inlines at compile time.

    Example: uint256 constant WAD = 1e18;

  10. Avoid redundant zero-initialization.

    Reason: default value is zero.

    Example: uint256 x; not uint256 x = 0;

  11. Short-circuit require early.

    Reason: stops costly work.

    Example: check balance before reading arrays.

  12. Use calldata slicing with care.

    Reason: avoid memory copies for ABI-encoded data.

    Example: parse selectors from msg.data when safe.

  13. Replace string compares with bytes32 tags.

    Reason: strings cost more to hash/compare.

    Example: bytes32 constant TAG = keccak256("FOO");

  14. Emit minimal event data.

    Reason: logs cost per byte and per topic.

    Example: index only what you query.

  15. Use custom errors instead of revert("message").

    Reason: shorter revert data.

    Example: error NotOwner();

  16. Prefer internal over public for library-like helpers.

    Reason: enables inlining.

    Example: function _calc(...) internal returns (...)

  17. Avoid abi.encodePacked when abi.encode fits.

    Reason: packed encoding can add keccak and padding work.

    Example: store structured data with encode.

  18. Use delete to reclaim storage gas refund when reset to zero.

    Reason: refunds reduce total gas.

    Example: delete user.allowance;

  19. Prefer mapping over array scans.

    Reason: O(1) access beats loops.

    Example: mapping(address => bool) isMember;

  20. Move constants to compile-time expressions.

    Reason: avoids runtime math.

    Example: precompute rates and scale factors.

  21. Use payable only when needed.

    Reason: small savings across calls.

    Example: non-payable for view-like writes.

  22. Consolidate modifiers or inline where simple.

    Reason: reduces extra jumps.

    Example: one onlyOwner check inside function.

  23. Avoid duplicate hashing.

    Reason: keccak256 is costly.

    Example: compute once, reuse the digest.

  24. Use assembly only for proven wins in hot spots.

    Reason: removes overhead, but needs care.

    Example: tight memcopy, optimized keccak preimage.

  25. Use bool sparingly in storage unless packed.

    Reason: can cost full slot; pack into uint256 bit 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.

Gas-saving patterns at a glance
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.

  1. Add a gas reporter to your test stack (Hardhat or Foundry).
  2. Write targeted tests that hit hot paths with realistic inputs.
  3. Record baselines before changes and commit the numbers.
  4. Apply one change at a time; re-run benchmarks.
  5. 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 unchecked where 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.