Typical Approaches to Reduce Contract Size and Gas Costs
Once you’ve completed your initial gas profiling, you can begin exploring strategies to reduce gas usage. There are numerous optimization techniques available, and this tutorial will highlight just a few foundational examples.
Enabling and Tuning the Optimizer
The gas report reveals that the Solidity optimizer is currently configured with 1,000 runs. While this setting can reduce runtime gas costs, it also increases the deployment cost of the contract. By lowering the number of optimizer runs—for example, to 200—you can reduce deployment costs. Here’s what that change looks like:
You can update hardhat.config.js:
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
After that run again npx hardhat test:

This change provides immediate improvements in deployment gas costs, although it may slightly increase the gas used during transaction execution.
Leveraging Immutable Variables
Within the Inventory contract, you may notice certain variables that are only assigned during contract deployment. These are ideal candidates for the immutable
keyword, which allows variables to be set once at construction time and then remain unchanged—offering gas savings by reducing storage reads.
By updating the Inventory
contract as follows:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract Inventory {
address public immutable manager;
uint256 public totalProducts;
struct Product {
uint256 id;
string description;
uint256 price;
}
mapping(uint256 => Product) public products;
constructor() {
manager = msg.sender;
}
function addProduct(string memory description, uint256 price) external {
require(msg.sender == manager, "invalid manager");
totalProducts++;
products[totalProducts] = Product(
totalProducts,
description,
price
);
}
}
Next, run the gas reporter again. You should observe the following output:

This already shows noticeable improvements.
Minimize Unnecessary Data Storage
Storing data directly on-chain within a smart contract is a key design decision that comes with both advantages and trade-offs.
On the pro side, keeping data in the contract ensures that all critical information is readily accessible and verifiable on-chain. This eliminates the need to rely on external services, off-chain databases, or event logs to retrieve or reconstruct the contract's state, which can enhance transparency and reliability.
However, the cons are significant. Storing large amounts of data on-chain increases gas consumption, making contract interactions more expensive for users. This can lead to high costs for both deployment and function execution, especially if frequent updates or large datasets are involved.
As a best practice, only essential data that must remain on-chain for security or verifiability should be stored in the contract. For non-critical or large data, consider using off-chain storage solutions (like IPFS or traditional databases) and referencing them via hashes or identifiers stored in the contract. This hybrid approach helps reduce gas costs while maintaining trust and transparency.
In the Inventory
smart contract, the following code is present:
struct Product {
uint256 id;
string description;
uint256 price;
}
mapping(uint256 => Product) public products;
Upon closer inspection, you'll notice that the id
field within the Product
struct and the key used in the mapping serve the same purpose. To eliminate redundancy, you can remove the id
from the Product
struct, as it's already represented by the mapping key.
The updated contract would look like this:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract Inventory {
address public immutable manager;
uint256 public totalProducts;
struct Product {
string description;
uint256 price;
}
mapping(uint256 => Product) public products;
constructor() {
manager = msg.sender;
}
function addProduct(string memory description, uint256 price) external {
require(msg.sender == manager, "invalid manager");
totalProducts++;
products[totalProducts] = Product(description, price);
}
}
When you execute the gas reporter again, the output will be:

This results in a further reduction in the gas consumption of the Inventory
smart contract. However, you can optimize even more by avoiding on-chain storage entirely for certain data. Instead of saving items in a mapping, you can emit events and treat them as a lightweight, cost-effective form of storage.
For example, you can update the contract as follows:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract Inventory {
address public immutable manager;
uint256 public totalProducts;
struct Product {
string description;
uint256 price;
}
event ProductCreated(uint256 id, Product product);
constructor() {
manager = msg.sender;
}
function addProduct(string memory description, uint256 price) external {
require(msg.sender == manager, "invalid manager");
totalProducts++;
emit ProductCreated(totalProducts, Product(description, price));
}
}
Notice that, rather than storing the items in state, the contract now emits an ItemCreated
event. This approach significantly lowers gas costs for both deployment and function execution:

As demonstrated, the gas savings from this approach are substantial. However, the trade-off is that to retrieve all items, you'll now need to parse through all ItemCreated
events emitted by the contract, as the data is no longer stored on-chain.
Using Custom Errors
Another effective method to reduce gas costs is to replace require
statements with custom errors. Custom errors are more gas-efficient, especially when used frequently. For example, you can refactor your code like this:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract Inventory {
address public immutable manager;
uint256 public totalProducts;
error InvalidManager();
struct Product {
string description;
uint256 price;
}
event ProductCreated(uint256 id, Product product);
constructor() {
manager = msg.sender;
}
function addProduct(string memory description, uint256 price) external {
if (msg.sender != manager) {
revert InvalidManager();
}
totalProducts++;
emit ProductCreated(totalProducts, Product(description, price));
}
}
This generates the following gas usage report:

Observe the reduction in deployment gas costs as a result of this change.
Conclusion
In this tutorial, you explored several practical strategies for profiling and optimizing the gas usage of your smart contracts using the Hardhat development framework and the Hardhat Gas Reporter plugin. By applying these techniques, you can build smarter, more efficient contracts that reduce transaction costs and improve the user experience. Lower gas fees not only make your contracts more accessible but also contribute to the overall scalability and sustainability of your decentralized application.
Last updated