🏠 Home
Beginner
01 — Introduction to Solidity 02 — Setting Up Your Environment 03 — Your First Smart Contract 04 — Data Types & Variables 05 — Functions & Visibility 06 — Control Flow 07 — Arrays & Mappings 08 — Structs & Enums
Intermediate
09 — Events & Logging 10 — Modifiers 11 — Inheritance 12 — Interfaces & Abstract Contracts 13 — Error Handling 14 — Ether & Wei 15 — Payable Functions 16 — msg.sender & msg.value 17 — Storage vs Memory vs Stack
Advanced
18 — Gas Optimization 19 — ERC-20 Tokens 20 — ERC-721 NFT Standard 21 — Contract Security 22 — Reentrancy Attacks 23 — Oracles & Chainlink 24 — Upgradeable Contracts 25 — Deploying to Mainnet
SolidityMaster / Lesson 18
Lesson 18 of 25

Gas Optimization

Patterns and techniques to dramatically reduce gas consumption in your contracts.

Advanced

Packing Storage Variables

The EVM stores data in 32-byte slots. If multiple variables fit in one slot, they share it — saving thousands of gas. Pack small types together.
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// ❌ Wasteful — each variable occupies a full 32-byte slot = 3 slots
contract Unpacked {
    uint256 a; // slot 0
    uint8   b; // slot 1 (wastes 31 bytes!)
    uint256 c; // slot 2
}

// ✅ Optimized — b and d share a slot with a smaller variable = 2 slots
contract Packed {
    uint128 a; // slot 0 (bytes 0-15)
    uint64  b; // slot 0 (bytes 16-23)
    uint64  c; // slot 0 (bytes 24-31)
    uint256 d; // slot 1
}

Use calldata for External Functions

For external functions that read but don't modify array/string parameters, use calldata instead of memory.
Solidity
// ❌ Copies data into memory — extra gas
function processMemory(uint256[] memory data) external pure
    returns (uint256 sum)
{
    for (uint i = 0; i < data.length; i++) sum += data[i];
}

// ✅ Reads directly from calldata — no copy
function processCalldata(uint256[] calldata data) external pure
    returns (uint256 sum)
{
    for (uint i = 0; i < data.length; i++) sum += data[i];
}

Custom Errors over String Messages

Custom errors (0.8.4+) are dramatically cheaper than string revert messages. The string is NOT stored on-chain at all — just a 4-byte selector.
Solidity
// ❌ String stored as calldata — expensive
function old() public {
    require(msg.sender == owner, "Ownable: caller is not the owner");
}

// ✅ Custom error — just a 4-byte selector
error NotOwner();
function modern() public {
    if (msg.sender != owner) revert NotOwner();
}

// Gas saved: ~200 gas per call, more with longer strings

Short-Circuit Evaluation & Loop Tips

Arrange conditions from cheapest to most expensive. Cache storage reads. Avoid dynamic-length loops.
Solidity
// ❌ Expensive: storage read happens even if first check fails
function bad(address user) public view returns (bool) {
    return isAdmin[user] || msg.sender == owner; // storage first
}

// ✅ Cheap: msg.sender is free, storage read only if needed
function good(address user) public view returns (bool) {
    return msg.sender == owner || isAdmin[user];
}

// Cache storage length outside loop
function sumOptimized(uint256[] storage arr) internal view
    returns (uint256 total)
{
    uint256 len = arr.length; // read once
    for (uint256 i; i < len;) {
        total += arr[i];
        unchecked { ++i; } // skip overflow check — safe here
    }
}