Create your own
Lesson illustration

Gas Optimization: Storage Packing & Calldata Efficiency

Hello! Welcome to your next lesson in the "Learn / recap Ethereum (Solidity) development in 2026" course.

Introduction

In our previous lessons, we've covered a range of topics from setting up development environments to implementing cross-chain communication. Now, we're shifting our focus from functionality to a crucial aspect of professional smart contract development: efficiency and cost.

Today's lesson addresses the learning outcome: "Apply gas optimization techniques using storage packing and efficient use of calldata."

We will explore two fundamental techniques that can significantly reduce the gas costs of your contracts. Given your extensive background in software development, you'll find that these low-level optimizations are about understanding the execution environment—in this case, the EVM—to write more performant code. We will cover:

  1. Storage Packing: How to arrange your contract's state variables to minimize storage costs.
  2. Efficient Calldata Usage: Understanding the difference between memory and calldata to reduce costs for function calls with complex arguments.

By the end of this 60-minute lesson, you'll be able to analyze and refactor Solidity code to make it cheaper for you to deploy and for your users to interact with.

Why Gas Optimization Matters

Before we dive into the techniques, let's quickly establish the context. Every operation on the Ethereum network, from a simple addition to writing data, has a cost measured in "gas." Your Solidity code is compiled into a series of low-level instructions called EVM opcodes, each with a specific gas cost. Gas optimization is the art of writing code that achieves the same result using a cheaper sequence of opcodes.

To get a concise overview of this concept, please watch the first two minutes of the following video.

Top 5 Tricks For Gas Optimizations in Solidity

This video from EatTheBlocks provides a great introduction to what gas optimization is, its objectives (deployment vs. execution cost), and the different approaches to achieving it.

Watch the section 'Introduction to Gas Optimization' from 00:36 to 02:19. Focus on the relationship between Solidity code, EVM opcodes, and gas costs.

As the video mentions, interactions with storage are among the most expensive operations. This makes storage optimization our first and most important topic.

1. Storage Packing: Making the Most of Expensive Real Estate

The EVM storage is like a vast, expensive warehouse where each storage "slot" is a 256-bit (32-byte) container. Writing to a new, empty slot (SSTORE opcode) is one of the costliest operations in the EVM.

Storage packing is the technique of arranging smaller state variables (like uint128, address, bool) so that the Solidity compiler can fit multiple of them into a single 32-byte slot. This can dramatically reduce both the deployment cost of your contract and the cost of functions that modify these variables.

To understand the rules and see a clear example, please read the following section from an article by Alchemy.

12 Solidity Gas Optimization Techniques

This article, '12 Solidity Gas Optimization Techniques', clearly explains the concept of variable packing with a practical before-and-after code snippet.

Read section #5, 'Pack your variables'. Pay close attention to the code examples showing inefficient vs. efficient variable declaration and the key packing rules listed.

The key takeaway is that variables are packed according to their declaration order. The compiler will try to fit variables into the current slot until the next one doesn't fit, at which point it moves to a new slot. A large variable type like a uint256 will occupy an entire slot and can break a packing sequence.

This image provides a helpful visual for how different data types can share a single 256-bit slot.

Caption: This diagram illustrates how multiple variables with smaller data types (e.g., `uint32`, `uint64`, `uint128`) can be packed by the Solidity compiler into a single 256-bit storage slot. This contrasts with a `uint256` variable, which consumes an entire slot on its own.

To see this in action and understand how to verify it, the next video offers a fantastic deep dive. It uses an assembly block to read storage slots directly, which is a powerful way to confirm how the EVM lays out your data.

Mastering Solidity Storage: Essential Mapping Secrets You Need to Know!

Jesper Kristensen's video 'Mastering Solidity Storage' provides a detailed, hands-on demonstration of how storage packing works under the hood.

Watch the segment 'Storage Packing for Gas Optimization' from 07:18 to 10:55. Notice how he declares two uint128 variables and then reads the storage slot to show they are packed together, whereas a uint256 occupies its own slot.

The same packing rules apply to members inside a struct. By ordering the fields of a struct from smallest to largest, you can often save storage slots.

2. Efficient Use of calldata

Our second optimization technique concerns how data is passed to functions. When you define a function that accepts an array, string, or struct, you must specify a data location for that parameter: memory or calldata.

  • calldata: This is a read-only data location where the arguments of an external function call are stored. Using calldata is cheap because your function reads the data directly from the call's payload without creating a copy.
  • memory: This is a temporary, modifiable data location. When you use memory for a parameter, the EVM first copies the data from calldata into memory. This copy operation costs gas, and the cost increases with the size of the data.

The Rule of Thumb:
For external functions, always use calldata for parameters that you only need to read. Only use memory if you absolutely need to modify the parameter within the function.

Let's start with a quick video explanation.

Top 5 Tricks For Gas Optimizations in Solidity

This clip from the EatTheBlocks video we saw earlier gives a very quick and clear explanation of when to use calldata.

Watch the short segment 'Optimizing Array Arguments with Calldata' from 03:39 to 04:13.

Now, let's reinforce this with a textual explanation and a code example.

12 Solidity Gas Optimization Techniques

The Alchemy article provides another excellent breakdown of calldata vs. memory, showing the gas savings with a concrete code example.

Read section #7, 'Store data in calldata instead of memory...'. Focus on the difference between the two code examples and the explanation of when memory becomes necessary.

Here is a practical example to solidify the concept:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract DataProcessor {
    // GOOD: This function only reads the array. `calldata` is most efficient.
    function sum(uint[] calldata numbers) external pure returns (uint256 total) {
        for (uint i = 0; i < numbers.length; i++) {
            total += numbers[i];
        }
    }

    // BAD: This function also only reads the array, but `memory` forces a
    // costly copy of the data before execution.
    function sumWithMemory(uint[] memory numbers) external pure returns (uint256 total) {
        for (uint i = 0; i < numbers.length; i++) {
            total += numbers[i];
        }
    }
    
    // NECESSARY: This function modifies the array. `memory` is required here
    // because calldata is read-only.
    function doubleInPlace(uint[] memory numbers) external pure {
        for (uint i = 0; i < numbers.length; i++) {
            numbers[i] *= 2;
        }
    }
}

For a large array, the gas difference between sum and sumWithMemory can be substantial.

Practice Challenge

Let's apply what you've learned. Consider the following unoptimized contract.

// Unoptimized Contract
contract TokenInfo {
    uint128 public reserveA;
    uint256 public totalSupply;
    address public owner;
    uint128 public reserveB;

    function checkAdmins(address[] memory admins) external view {
        // ... some logic that only reads from the admins array
    }
}

Based on today's lesson, answer these three questions:

  1. As written, how many storage slots do the state variables (reserveA, totalSupply, owner, reserveB) consume? Why?
  2. How would you reorder the state variables to pack them efficiently? How many slots would the optimized version use?
  3. What single-word change can you make to the checkAdmins function to make it more gas-efficient, and why?
Click to reveal the answers
  1. 4 storage slots. The variables are not ordered for packing.

    • reserveA (16 bytes) takes Slot 0.
    • totalSupply (32 bytes) cannot be packed with reserveA, so it takes a new slot, Slot 1.
    • owner (20 bytes) takes a new slot, Slot 2.
    • reserveB (16 bytes) takes a new slot, Slot 3.
  2. Optimized Order:

    contract TokenInfo {
        uint128 public reserveA; // 16 bytes
        uint128 public reserveB; // 16 bytes
        address public owner;    // 20 bytes
        uint256 public totalSupply; // 32 bytes
    }
    

    This optimized version uses 3 storage slots.

    • reserveA and reserveB are declared consecutively and their combined size (16 + 16 = 32 bytes) fits perfectly into Slot 0.
    • owner (20 bytes) takes the next slot, Slot 1.
    • totalSupply (32 bytes) takes Slot 2.
      This saves one SSTORE operation on deployment and can save gas on subsequent state changes.
  3. Change address[] memory admins to address[] calldata admins. Since the function is view and only reads the admins array, using calldata avoids the expensive operation of copying the array from the transaction payload into memory.

Conclusion

Excellent work! You've just covered two of the most impactful gas optimization techniques in Solidity. These practices are standard in professional development, where minimizing transaction costs is essential for user experience and protocol viability.

Key Takeaways:

  • Storage is the most expensive resource. Always be mindful of what you store on-chain.
  • Pack Your Variables: Arrange state variables (and struct fields) to group smaller data types together. This allows the compiler to fit multiple variables into a single 32-byte storage slot, saving significant gas.
  • Use calldata by Default: For external function parameters like arrays, strings, and structs, prefer calldata to avoid costly data copying. Only switch to memory when you need to modify the data within the function.

Next Lesson Preview:

In our next lesson, we will venture deeper into low-level optimization by learning how to: "Rewrite a Solidity function using a Yul assembly block to reduce gas costs for complex operations." This will give you fine-grained control over the EVM, unlocking even greater potential for gas savings in performance-critical parts of your contracts.

Can't find a good explanation? Sign up and we'll make it for you

Sign up