Top 10 Gas Optimization Techniques for Developers

HomeTechnologyBlockchainTop 10 Gas Optimization Techniques for Developers

Must read

Gas costs define how efficient and sustainable your smart contracts are, especially at scale. Development choices that feel small during coding can compound into heavy costs in production. This guide lists the Top 10 Gas Optimization Techniques for Developers and explains the thinking behind each technique, from storage patterns to compiler settings. You will learn how to reduce expensive writes, arrange data for cheaper access, and avoid unbounded work that can trap users. The goal is to help both new and experienced engineers make deliberate, measurable improvements without sacrificing correctness, readability, or security. Use these techniques as a checklist during design, coding, and review.

#1 Choose cheaper data locations and types

Use the cheapest data location that still preserves correctness. Prefer calldata for external function parameters because calldata avoids a runtime copy. Use memory only when mutation or reuse across calls is required. Touch storage sparingly, since each read and especially each write costs much more. Pick fixed size types where practical, such as bytes32 over bytes and uint256 over smaller integers on EVM chains that widen anyway. Use mappings instead of arrays for existence checks. When passing arrays, mark them as calldata and avoid converting to memory unless necessary to simplify logic. Cache often used storage reads in memory for the current call.

#2 Pack storage and design tight struct layouts

Storage slots are 32 bytes, so packing variables to fit within a slot lowers gas by reducing the number of touched slots. Order struct fields from largest to smallest and group types with compatible sizes so that multiple values share a slot when possible. Avoid mixing dynamic types with frequently updated values inside the same struct, because changes can cascade into additional writes. Prefer uint256 and bytes32 for alignment unless packing provides a clear win. For booleans, use uint8 or a bitmap technique. Minimize writes by updating an in memory copy, then writing the packed result once.

#3 Minimize SSTORE calls and batch state updates

SSTORE is the most expensive common operation. Reduce writes by computing results in memory, then performing a single consolidated write to storage. Avoid writing the same value again if it has not changed. Consider delete only when it meaningfully refunds gas under current rules, since refunds are capped. Where appropriate, maintain incremental aggregates so that you update one counter instead of a large structure. When several related fields change together, write them in one packed struct. Cache storage reads into local variables to prevent repeated SLOADs, especially inside loops or conditional branches. Do writes after validations to avoid paying for changes that would revert.

#4 Use constants and immutables for cheap access

Constants are inlined at compile time and do not occupy storage, which avoids both SLOAD and SSTORE. Mark configuration values as constant when they never change, such as mathematical factors, role identifiers, and addresses that are hard coded. When a value is set once in the constructor and stays fixed, mark it as immutable. Immutable values are stored in bytecode and read with low cost during execution. Replacing repeated literals with constants also improves readability. Be careful not to mark values constant if they might need governance updates, or you will need a redeploy to change them.

#5 Prefer custom errors and precise reverts

Using custom errors reduces deployment and runtime gas compared to long revert strings. Define clear error types with parameters that capture the failing state, then use revert ErrorName(args) for checks. Validate early to fail fast before any storage writes. Group related assertions so that you do not repeat the same expensive checks. Return informative parameters rather than verbose messages to keep bytecode slim. Restrict error usage to exceptional paths so that the happy path remains short. Short revert reasons in require are acceptable for development, but custom errors are superior in production deployments. Add boundary checks to catch mistakes before deeper logic runs.

#6 Tune function visibility and call patterns

Public functions copy calldata to memory for arguments, while external functions can read directly from calldata, which often saves gas. Mark functions external when they are not called internally. Use internal helpers to share logic without incurring dynamic dispatch overhead. Prefer pure and view where possible, since they signal no state writes and sometimes unlock compiler optimizations. Avoid heavy modifiers that duplicate logic across functions. Use a private internal precheck function and call it from places that need it. Eliminate unused return values and parameters. Keep interfaces minimal so that proxy or external calls carry less encoding and decoding cost.

#7 Optimize loops and control flow constructs

Unbounded loops create gas risk and unpredictable costs. Design data flows that avoid iterating over user controlled lengths on chain. When a loop is required, cache array length and common values in local variables. Use unchecked increments where overflow is impossible by design to reduce arithmetic checks. Short circuit conditionals so that cheap tests run before expensive ones. Break early when the answer is known. Replace linear searches with mappings or indexes built at write time. Consider off chain precomputation for lists, then verify with proofs on chain to avoid heavy iterations. For batch work, process fixed size chunks per transaction to keep costs predictable.

#8 Prefer events for logging over redundant storage

Writing to storage to preserve a historical record is expensive and often unnecessary. Emit events to create an auditable log that off chain indexers can process cheaply. Keep only the minimal state required for correctness in storage, such as balances and critical configuration. Use indexed event parameters to enable efficient filtering by clients. Avoid emitting duplicate data that is already available from state. Do not read from events in contracts, since they are not accessible on chain. Use events as a complement to lean state, not a replacement for it, and document how clients should consume them.

#9 Reduce deployment size and reuse code effectively

Deployment gas matters for upgrades and for systems that spawn many instances. Use libraries to share reusable logic across contracts and avoid duplicating bytecode. Adopt minimal proxy patterns such as EIP 1167 clones for factories that create many contracts with the same logic. Keep constructor work minimal by moving heavy initialization to an explicit init function that runs once. Strip dead code, debug helpers, and unused features before release builds. Prefer small, purpose built contracts that compose rather than a monolith. Lean deployments reduce bytecode size, which can lower call costs through shorter jump tables and simpler paths.

#10 Leverage the compiler optimizer and targeted assembly

Enable the Solidity optimizer with runs tuned to your workload. High runs often help read heavy contracts, while lower runs can suit write heavy paths. Benchmark with a gas snapshot test suite to select a configuration based on evidence. Use inline assembly only for small hot paths where the gain is clear and the logic is simple. Document assembly carefully and add thorough tests, since you bypass safety checks. Prefer built in opcodes through Solidity when they compile well. Re evaluate gas after each significant change, since small edits can shift optimizer behavior and costs.

More articles

Latest article