A Friendly Guide to Optimizing Gas Fees in Solidity

Harsh PanghalHarsh Panghal
6 min read

Ethereum gas fees remain a top concern—even after its shift to Proof‑of‑Stake. Gas optimization is more than just a hack—it’s a core discipline of smart contract engineering. This article breaks down the "what", "why", and "how" of gas optimization in Solidity with real-world examples, theory, and 10 essential best practices you can use today to reduce Ethereum transaction costs and improve your contract performance.

By the end, you’ll know how to write cleaner, faster, and cheaper Solidity code—and help the entire ecosystem along the way.

What is Gas in Ethereum?

In the Ethereum ecosystem, gas is the unit used to measure computational effort. Every operation in the Ethereum Virtual Machine (EVM) consumes gas, whether it's adding two numbers, storing data, or calling another contract.

Think of gas like fuel: the more complex the task, the more fuel it requires. You pay for this fuel in ETH, and prices vary based on network demand.

Without gas, Ethereum smart contracts couldn’t operate—it’s what powers every decentralized interaction.

Why Should You Optimize Gas in Solidity?

Gas fees have two major pain points:

  1. Cost to Users: High gas fees make your dApp unaffordable.

  2. Block Limits: Ethereum blocks have a gas cap. Contracts using too much gas can fail or be excluded from blocks.

Optimizing gas:

  • Reduces user friction

  • Saves ETH for you and your users

  • Makes your contract more scalable

  • Prevents out-of-gas errors

  • Encourages cleaner, more maintainable code

💡 Gas optimization isn’t just about reducing costs—it’s about writing smarter, faster, more secure smart contracts.

Deeper Dive: How Solidity Code Consumes Gas

Solidity compiles down to byte code, which runs on the EVM. Each line of Solidity translates into one or more EVM opcodes—basic machine instructions like ADD, STORE, CALL.

Each opcode has a fixed gas cost. For example:

OpcodeDescriptionGas Cost
ADDAddition3
SSTOREStore in storage20,000+
CALLExternal function call700–2600

The SSTORE opcode is one of the most expensive. That’s why you'll often see optimization strategies focused on minimizing storage operations.

1. Use Mappings Instead of Arrays

Arrays require iteration. Mappings act like hash maps and offer O(1) lookup time without iteration. Since EVM charges gas for every computational step, avoiding loops saves gas.

// Less Optimal
string[] public cities = ["NY", "Moscow", "HK"];

// Optimized
mapping(uint => string) public cities;

Use mappings whenever iteration isn’t needed—access is O(1), and it’s cheaper than looping through arrays.


2. Enable the Compiler Optimizer

The compiler optimizer simplifies expressions, inlines functions, and minimizes opcode count. More optimization runs = better runtime efficiency, but bigger contract size.

Config (Hardhat):

optimizer: {
  enabled: true,
  runs: 10000
}

Higher runs mean more deployment cost, but cheaper execution later on. Low runs is the opposite. Find the right balance. Use runs: 200 for short-lived contracts; use 10000+ for production-grade contracts used frequently.


3. Minimize On‑Chain Data

Storage is the most expensive part of Ethereum. Instead of storing everything on-chain, keep large data structures off-chain and only store critical state.

Tactics:

  • Only store essential state on‑chain.

  • Use off‑chain storage and on‑chain verification (oracles…).

  • Use IPFS, The Graph, or Chainlink for external data.

  • Batch transactions using dynamic arrays.

  • Avoid loops in your contract, they can exceed block gas limits.

Batch operations to rebuild loops outside—e.g.,

function batchSend(Call[] calldata calls) external { … }

4. Use Indexed Events

Index your event fields to let off‑chain tools track contract activity cheaply:

event Transfer(address indexed from, address indexed to, uint256 value);

Indexed events are searchable and don’t bloat your contract state. Don’t use events for storing critical state—they are not accessible on-chain by contracts.


5. Be Careful with uint8 and Small Types

The EVM works in 32-byte words. Using smaller types (e.g. uint8, uint16) individually can cost more because they need to be padded to 32 bytes.

Use smaller types only when packing multiple values.


6. Pack Variables

EVM packs consecutive small variables into the same storage slot. State variables are stored in 32-byte slots. If multiple variables fit into a single slot, EVM can store them together, saving gas.
Bad example:

// Not packed
uint128 a;
uint256 b;
uint128 c;

Optimized:

// Packed
uint128 a;
uint128 c;
uint256 b;

Keeps storage compact = less gas.


7. Free Up Unused Storage

Resetting variables or using delete refunds gas because it clears state storage.

delete myStruct;
delete myVar;
myInt = 0;

(Note: mappings don’t auto-delete—delete entries manually.)

Mappings don’t benefit from this, but structs and arrays do.


8. Prefer calldata Over memory for Read‑Only Arrays

Memory is copied from calldata. If you're not modifying a parameter (e.g. arrays), using calldata avoids unnecessary copying.

function process(uint[] calldata arr) external { … }

Saves gas, especially with large inputs.


9. Use immutable & constant

constant and immutable variables are inlined or stored cheaper. Saves gas!

solidityCopy codeuint256 constant FEE = 10;
uint256 immutable owner;

Immutable: Set once during deployment.
Constant: Fixed at compile time.


10. Use external Instead of public for External Calls

function foo() external view returns (...) { … }

external is cheaper for public-facing functions. Use public only when internal calls are necessary.


Final Tip: Always Test Gas Usage

Use tools like Hardhat gas reporter and test in Rinkeby/Mainnet fork to see real costs—especially for public functions like mint().

Use tools like:

  • Hardhat’s gasReporter

  • Remix’s compiler estimates

  • Tenderly’s gas profiler

Focus optimization on functions called frequently—like mint, buy, vote, etc.


Summary Table

Optimization StrategyBenefit
Mappings vs ArraysO(1) lookup, no loops
Compiler Optimizer (runs)Smarter, leaner bytecode
Minimize On‑chain StorageLighter state, lower costs
Indexed EventsSearchable, efficient logging
Avoid Small Integers in LoopsUse 256‑bit for performance
Variable PackingCompact storage
Delete Unused StorageGas refunds
calldata over memoryCheaper parameter passing
constant & immutableInlined/optimized storage
external functionsLower call overhead

Optimizing gas in Solidity isn’t a “nice to have.” It’s part of writing professional-grade smart contracts. Your users will thank you, your protocol will scale better, and you’ll reduce your project’s environmental and economic cost.

Start small: pick one tip and apply it today. Once you test and confirm savings, move to the next. Before you know it—you’re writing top-tier optimized contracts!

0
Subscribe to my newsletter

Read articles from Harsh Panghal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harsh Panghal
Harsh Panghal