本文介绍了 7 种实用的 Solidity Gas 优化技术,包括减少链上存储、使用 calldata 替代 memory、利用常量与不可变量、缓存存储读取、优先使用映射、采用自定义错误以及优化循环设计,旨在帮助开发者降低智能合约交互成本并提升系统性能。

Gas 优化曾经只是开发者担心的事。但现在情况已经不同了。
在 Web3 中,每一次交互都有成本。如果你的合约效率低下,用户会立即感受到 —— 他们为每笔交易支付更多费用。随着时间的推移,这种摩擦会不断累积。它会影响人们使用你产品的频率、他们是否留下来,甚至影响投资者如何评估你正在构建的东西。
到 2026 年,这一点变得更加明显。有更多的产品在竞争注意力,用户期望交易便宜,甚至在 Layer 2 上,低效率的合约仍然显得格格不入 —— 以一种糟糕的方式。
但 Gas 优化并不是要编写聪明的技巧或在每个地方压榨微小的收益。它实际上是关于从头开始设计高效的系统。
本指南介绍了在现实世界的 Solidity 开发中真正发挥作用的七种实用技术。
如果有一件事需要记住,那就是:存储是昂贵的。
写入存储的 Gas 成本显着高于使用内存的成本。在许多合约中,这就是大部分不必要成本的来源。
struct User {
uint256 balance;
uint256 timestamp;
}
mapping(address => User) public users;
function updateBalance(uint256 amount) public {
users[msg.sender].balance = amount;
}
与其存储所有内容,不如询问哪些内容真正需要留在链上。
event BalanceUpdated(address user, uint256 amount);
function updateBalance(uint256 amount) public {
emit BalanceUpdated(msg.sender, amount);
}
许多数据不需要永远存储在链上。日志(事件)通常就足够了,尤其是与索引器结合使用时。
一个简单的习惯对此有所帮助:在写入存储之前,停下来问一问 —— 这是否需要持久化在链上,还是仅仅被记录下来?
仅减少不必要的规范状态更改就可以大幅降低 Gas 成本。
calldata 代替 memory这是那些悄悄产生巨大影响的小改变之一。
当你对函数输入使用 memory 时,Solidity 会创建数据的副本。该副本会消耗 Gas。而使用 calldata,函数会直接从交易输入中读取。
function process(uint256[] memory data) public {
// 逻辑
}
function process(uint256[] calldata data) external {
// 逻辑
}
对于大型数组或批量操作,差异变得更加明显。你避免了不必要的复制,从而保持了较低的执行成本。
在实践中,一个好的规则是:如果数据不需要修改,请使用 calldata。
constant 和 immutable并非每个变量都需要留在存储中。
如果一个值永远不会改变,存储它就是一种浪费。Solidity 为你提供了更好的选择。
uint256 public fee = 100;
address public owner;
constructor() {
owner = msg.sender;
}
uint256 public constant FEE = 100;
address public immutable owner;
constructor() {
owner = msg.sender;
}
常量和不可变量直接嵌入到合约字节码中。这意味着没有存储插槽,并且在部署和执行期间的 Gas 消耗更低。
这是一个简单的改变,但在一个大型系统中,它会迅速累积。
从存储中读取并不是免费的 —— 重复读取甚至更糟。
function getBalance() public view returns (uint256) {
return balances[msg.sender] + balances[msg.sender];
}
function getBalance() public view returns (uint256) {
uint256 balance = balances[msg.sender];
return balance + balance;
}
每次存储读取都有成本。如果你多次读取相同的值,你每次都要为此付费。
将其在内存中缓存一次可以避免这种重复。这是一个很小的优化,但在真实的合约中非常常见 —— 尤其是在循环内部。
数组看起来很简单,但它们的成本会迅速增加 —— 尤其是当你需要搜索它们时。
address[] public users;
function findUser(address user) public view returns (bool) {
for (uint i = 0; i < users.length; i++) {
if (users[i] == user) return true;
}
return false;
}
mapping(address => bool) public isUser;
function checkUser(address user) public view returns (bool) {
return isUser[user];
}
映射为你提供即时访问 (O(1)),而数组需要迭代。循环是增加 Gas 使用量甚至触及 Gas 限制的最快方式之一。
即便如此,当顺序或迭代很重要时,数组仍然是有意义的。关键是为工作选择正确的结构。
以字符串形式编写的错误消息出奇地昂贵。
require(balance >= amount, "Insufficient balance");
error InsufficientBalance(uint256 balance, uint256 amount);
if (balance < amount) {
revert InsufficientBalance(balance, amount);
}
自定义错误使用更少的数据并减小合约大小。随着时间的推移,特别是在具有许多检查的合约中,这将带来显着的节省。
记住我以便更快登录
这已成为现代 Solidity 中的标准做法,原因很简单 —— 它更简洁、更便宜。
许多 Gas 效率低下的问题源于最初看起来不重要的小设计选择。
function process(uint256[] memory data) public {
for (uint i = 0; i < data.length; i++) {
// 逻辑
}
}
function process(uint256[] calldata data) external {
uint length = data.length;
for (uint i = 0; i < length; ) {
// 逻辑
unchecked { i++; }
}
}
external 比 public 更便宜length 避免了重复读取unchecked 移除了溢出检查单独来看,这些都是很小的改进。合在一起,它们可以产生显著的差异 —— 尤其是在高频函数中。
过度优化是很诱人的,但这往往是出错的地方。
unchecked从正确性开始。然后进行优化。
可读、结构良好的代码更容易审计和维护。在 Web3 中,这与节省几个单位的 Gas 同样重要。
没有测量的优化大多是猜测。
一些值得使用的工具:
这些工具可以帮助你识别真正的瓶颈,而不是追求微小的改进。
很容易陷入微优化中。但更大的收益通常来自设计决策。
设计良好的系统自然具有更高的 Gas 效率。
Gas 优化是那些容易被忽视的事情之一 —— 但一旦用户开始付出代价,就很难被忽视。
不优先考虑优化的项目往往会:
做得好的项目:
在这一点上,它已不再是可选的。它是预期的。
在 Ancilar,重点是从一开始就构建高效的系统,而不是事后才考虑。
这意味着超越功能性,审视合约在实际使用下的表现:
目标不仅仅是让事情运转起来,而是让它们好用。
如果你正在构建 Web3 产品并希望你的合约高效、可扩展且准备好投入生产,你可以在这里联系:
https://www.ancilar.com/contactUs
- 原文链接: medium.com/@ancilartech/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!