---
title: "Solidity gas 優化：12 種讓智能合約更省 gas、更高效的技巧"
description: "想寫出更好的程式碼並降低 gas 費用？"
---

# Solidity gas 優化：12 種讓智能合約更省 gas、更高效的技巧

<ImageBlock
  src="https://media.alchemy.com/1763564597-blog-gas-optimization.png"
  alt="加油站插圖，代表 Solidity gas 優化"
  width={2880}
  height={1620}
  priority
/>

Ethereum 的 gas 費用一直是這個生態系統的痛點：有誰會想為一筆簡單的鏈上交易支付 $20\+ 的 gas？雖然過去幾年 Ethereum 的多次升級大幅降低了使用者的 gas 成本，但優化 [Solidity](https://www.alchemy.com/overviews/solidity) 程式碼仍然是讓你的應用能在不讓使用者破產的前提下，實現複雜鏈上操作的主要方法。

無論你是在程式碼審查中降低風險，還是單純想寫出更乾淨的合約，做好 gas 優化就代表你能打造安全、可負擔且能擴展到數百萬使用者的應用。在這篇指南中，我們會拆解 gas 的基本概念、為什麼優化很重要（劇透：相較於未優化的程式碼，可以省下 20-50% 的成本），並分享 12 種附帶程式碼範例的優化技巧。

## 什麼是 Solidity 中的 gas 與 gas 優化？

[Gas](https://ethereum.org/developers/docs/gas/) 是衡量在 Ethereum 上執行特定操作所需計算量的單位，而 Solidity 的 gas 優化，就是讓你的 Solidity 智慧合約程式碼執行成本更低的過程。

在 Ethereum 上進行交易時，每筆交易都有其成本——寫入資料到儲存空間，或處理一筆交易的成本，這個成本就被稱為「gas」。如果你不支付 gas 費用，什麼事都不會發生，就像汽車需要汽油才能行駛一樣。

合約也需要 gas。以下是部署與執行智慧合約時發生的過程：

1. 你撰寫[**Solidity 程式碼**](https://www.alchemy.com/overviews/solidity-smart-contract)。這是為 Ethereum 智慧合約設計的高階、人類可讀的程式語言。
1. **編譯器將其轉換成 bytecode。** 當你編譯 Solidity 程式碼時，編譯器會將其轉譯成 bytecode，一種以十六進位表示的低階指令。這個 bytecode 就是實際儲存在區塊鏈上的內容。
1. **Bytecode 由 opcode 組成。** 這個 bytecode 是由 opcode（操作碼）組成的，opcode 是 EVM 的指令集：可以把它想成是 Ethereum 的組合語言。每個 opcode 代表一個特定操作，例如「將兩個數字相加」、「讀取儲存空間」，或「跳到另一個指令」。
1. **EVM 執行 opcode。** 當有人呼叫你的智慧合約時，網路上各節點運行的 [Ethereum Virtual Machine](https://www.alchemy.com/overviews/what-is-the-ethereum-virtual-machine-evm)（EVM）會讀取 bytecode，將其解碼成個別的 opcode，並逐一執行。每個 opcode 都有固定的 gas 成本。

舉例來說，`ADD` opcode（用來將兩個數字相加）的成本是 3 gas，而 `SSTORE`（用來寫入儲存空間）的成本至少是 20,000 gas。EVM 會加總交易執行過程中每個 opcode 的 gas 成本。

想了解更多 Ethereum gas 模型的內容，可以參考[官方文件](https://ethereum.org/en/developers/docs/gas/)。

Gas 優化就是調整你的程式碼，讓它用更少的操作完成同樣的工作，藉此降低執行成本。如同上面提到的，每筆交易都需要 gas（以 ETH 或其他鏈上的等價物支付）。在 [Dencun](https://consensys.io/ethereum-dencun-upgrade) 之後，得益於 blob，資料可用性的成本已經變便宜，但執行 gas（也就是運算成本）仍然可能累積成一筆不小的開銷。優化過的合約不僅能替使用者省錢，較精簡的程式碼庫也能防範 DoS 攻擊。想深入了解 opcode，可以參考 [「Gas Optimizer」文件](https://docs.soliditylang.org/en/latest/internals/optimizer.html)。

## 為什麼 gas 優化對開發者來說很重要？

高 gas = 令人沮喪的使用體驗，可能讓使用者放棄你的應用，尤其是在流量暴增、gas 費用飆升的時候。

優化程式碼以降低 gas 用量，可以降低使用者的費用、提供更好的使用體驗，並讓原本因費用過高而不划算的小額交易變得可行，同時也能在高使用量時避免碰到區塊 gas 上限。

未優化的合約可能白白多消耗 [20-50% 的 gas](https://www.cs.toronto.edu/~fanl/papers/gas-brain21.pdf)，推高成本並增加被攻擊的機會。截至 2025 年 10 月，DeFi 的總鎖倉量已接近 [1,500 億美元](https://defillama.com/)，gas 效率高的合約已不只是「有的話不錯」，而是能左右使用者採用與否的競爭優勢。

要嘛你的應用有複雜的智慧合約邏輯，讓使用者為這種複雜度付出高額費用；要嘛你優化程式碼，讓使用者的最終體驗更好、更便宜。

## 12 個頂尖 Solidity gas 優化技巧

以下是經過實戰驗證的程式碼 gas 優化方法。我們會針對每個範例說明其原理、如何節省 gas，並附上程式碼。建議你在 Remix 或 Hardhat 中親自測試，感受其中的差異。

### 1. 使用 mapping 而非 array

Solidity 提供兩種主要的資料結構來儲存資料清單：array 與 mapping。雖然語法看起來相似，但它們的用途截然不同，gas 成本也差異極大。

Array 是有序、可迭代的集合，元素依序儲存在記憶體或儲存空間中。當你需要遍歷所有項目或維持特定順序時，array 就很有用。然而，在 array 中尋找特定項目需要迭代：EVM 必須逐一檢查每個元素，直到找到符合的項目。這代表查找操作的複雜度是 O\(n\)，array 越大，成本就越高。

Mapping（也稱為雜湊表）的運作方式完全不同。它們採用鍵值結構，讓你可以透過鍵直接取得任何值，無論儲存了多少項目，查找時間都是常數 O\(1\)。這是因為 Solidity 使用雜湊函式直接從鍵計算出儲存空間位置，不需要搜尋資料。代價是 mapping 不可迭代，你無法遍歷所有項目，也無法在不另外追蹤的情況下知道有哪些鍵存在。

**對 gas 的影響**：建立與存取 mapping 項目的成本，遠比 array 操作便宜，因為沒有迭代的額外開銷。只有在你確實需要遍歷所有項目或維持插入順序時，才使用 array。對於其他所有情況，特別是使用者餘額、擁有權記錄，或任何以鍵為基礎的查找：mapping 明顯是更好的選擇。

以下是使用 array 的範例（存取成本較高）：

<CodeSnippet language="solidity" code={`string[] public cars = ["ford", "audi", "chevrolet"];

// To find "audi", you'd need to loop through the array
function findCar(string memory target) public view returns (bool) {
for (uint i = 0; i < cars.length; i++) {
if (keccak256(bytes(cars[i])) == keccak256(bytes(target))) {
return true; // Cost increases with array size
}
}
return false;
}`} />

以下是使用 mapping 儲存相同資料的範例（查找成本便宜許多）：

<CodeSnippet language="solidity" code={`mapping(uint => string) public cars;

constructor() {
cars[101] = "Ford";
cars[102] = "Audi";
cars[103] = "Chevrolet";
}

// Direct access - O(1) constant time regardless of data size
function getCar(uint id) public view returns (string memory) {
return cars[id]; // Single storage read, minimal gas
}`} />

使用整數作為鍵，可以在不用承擔 array 迭代成本的情況下模擬有序清單。這對於使用者資料（如餘額或擁有權記錄）特別有用，因為你需要透過 ID 快速直接存取。想更深入了解 mapping 底層的運作方式，可以參考 [Solidity mapping 文件](https://docs.soliditylang.org/en/latest/types.html#mapping-types)。

### 2. 啟用 Solidity 編譯器優化器

[Solidity 編譯器優化器](https://docs.soliditylang.org/en/latest/internals/optimizer.html)是能大幅降低 gas 成本的強大工具，但需要根據你的實際使用情境進行設定。優化器的運作方式是分析你的程式碼並套用各種轉換：簡化表達式、移除無用的程式碼、將小型函式內聯以消除昂貴的跳轉操作，並重複使用重複的程式碼區段。這些改變都能減少 EVM 需要執行的 opcode 數量。

不過，這裡有一個由「runs」參數控制的重要取捨。這個數字告訴優化器，你預期合約中每個 opcode 在其生命週期中會被執行多少次。優化器會用這個數字，在兩個互相競爭的目標之間取得平衡：最小化部署成本（只發生一次）與最小化執行時的執行成本（每次函式呼叫都會發生）。

runs 參數的運作方式：

- **低 runs（例如 200）**：優化器會優先考慮較小的 bytecode，讓部署成本更低。這代表較少的程式碼重複，以及在可重複使用的程式碼區段之間有更多跳轉。最適合你會頻繁部署但很少呼叫的合約——例如工廠合約或一次性使用的部署腳本。
- **高 runs（例如 10,000\+）**：優化器會優先考慮執行效率，透過重複程式碼避免跳轉，並大量內聯函式。這會產生較大的 bytecode（部署成本較高），但函式執行速度更快、成本更低。最適合交易量高的合約：例如 DEX 路由器、質押合約，或 NFT 市場。

以下是部署頻繁的應用使用低 runs（200）的範例：

<CodeSnippet
  language="javascript"
  code={`module.exports = {  solidity: {    version: "0.8.9",    settings: {      optimizer: {        enabled: true, // Set to true for opt        runs: 200,      },    },  },};`}
/>

以下則是部署頻率低、針對高 runs（10000）優化的應用範例：

<CodeSnippet
  language="javascript"
  code={`module.exports = {  solidity: {    version: "0.8.9",    settings: {      optimizer: {        enabled: true,        runs: 10000,      },    },  },};`}
/>

#### 選擇合適的 runs 值

兩種做法各有取捨。想想你的合約生命週期。一個只部署一次但有數百萬筆轉帳交易的治理代幣，應該使用高 runs。而一個不斷建立新合約、但這些合約很少被呼叫的部署工廠，則應該使用低 runs。如果不確定，200 是一個能合理平衡兩者考量的安全預設值。你可以根據自己合約的特性進行測試，並在 [Hardhat 設定](https://hardhat.org/hardhat-runner/docs/config#solidity)或 Remix 中啟用任一選項。

### 3. 減少鏈上資料

鏈上儲存是 Solidity 中成本最高的單一操作。每次 `SSTORE` opcode（寫入儲存空間）的成本可能超過 20,000 gas（以 Gwei 計算），而修改既有的儲存空間位置仍需要 2,900-5,000 gas。相比之下，記憶體操作只需要 3 gas，這也是為什麼儲存優化如此重要。這裡的基本原則很簡單：只在鏈上儲存絕對必要的資料，其他一切都透過 API、oracle 或索引服務在鏈下處理。

除了減少儲存寫入，你也應該聰明地將操作分組，以避免重複的成本。這能讓你的合約更精簡，同時降低部署與執行費用，也讓攻擊者更難利用可能被用於 DoS 攻擊的高運算量函式。

#### 將資料儲存在儲存變數中

對於什麼值得儲存要嚴格把關。舉例來說，使用者餘額、擁有權記錄，以及必須防篡改且全域可存取的合約狀態：這些都應該放在鏈上。如果要優化 gas，其他一切都應該放在鏈下。舉例來說，如果你需要外部價格資料，應該使用 Chainlink 這類 oracle 在執行時取得資料，而不是儲存歷史價格。如果你需要為前端追蹤交易歷史，應該發出事件，而不是儲存 array。

事件有一個關鍵的陷阱：雖然發出事件的成本很低（基礎約 375 gas，每個 topic 額外 375 gas），但合約無法讀取自己發出的事件。事件的存在純粹是為了讓鏈下的索引器與前端使用。絕對不要把事件當作合約邏輯需要存取的儲存空間的替代品。

#### 批次處理操作

與其要求使用者提交多筆各自獨立的交易，不如將相關的操作打包成單一交易。這樣能省下 21,000 gas 的基礎交易費（無論交易做什麼，每筆交易都要付這個費用），也能減少重複的操作，例如多次檢查 `msg.sender`、重複載入相同的儲存變數，或為多次交易提交支付 calldata 成本。

這種模式對多步驟流程特別有用，例如先進行代幣授權再轉帳，或原子化地執行多個 DeFi 操作（先兌換，再質押，然後領取獎勵）。

以下是批次發送函式的範例：

<CodeSnippet language="solidity" code={`Struct Call {
address recipient;
uint256 gas;
uint256 value;
bytes data;
}

function batchSend(Call[] memory \_calls) public payable {
for(uint256 i = 0; i < \_calls.length; i++) {
(bool \_success, bytes memory \_data) = \_calls[i].recipient.call{
gas: \_calls[i].gas,
value: \_calls[i].value
}(\_calls[i].data);

    if (!_success) {
        assembly {
            revert(add(0x20, _data), mload(_data))
        }
    }

}

}`} />

這種模式透過消除重複的 `msg.sender` 驗證、減少 calldata 開銷（函式選擇器只需傳遞一次），以及只支付一次基礎交易費而非每次操作都支付，大幅節省 gas。

#### 迴圈

迴圈是 gas 的倍增器，每次迭代都會重複相同的操作，成本會線性累加。對 100 個項目進行儲存操作的迴圈，可能輕易消耗超過 500,000 gas，而對無邊界 array 的迴圈甚至可能超過區塊 gas 上限，讓你的函式永遠無法被呼叫。

幾乎所有情況下，解決方法都是完全消除迴圈。使用 mapping 進行常數時間 O\(1\) 查找，而不是 O\(n\) 的 array 迭代。如果真的必須迭代，請嚴格限制 array 大小，或者更好的做法是，將迭代移到鏈下，讓使用者提交特定的索引或鍵。

#### 關於 gas 退款的說明

Solidity 過去會針對清空儲存空間（將值設為零）提供 gas 退款，但 EIP-3529 大幅減少了這些退款。雖然清空儲存空間仍然會有少量退款，但它已不再是主要的優化策略。想了解目前退款機制的詳細內容，可以參考 [Ethereum gas 退款提案](https://eips.ethereum.org/EIPS/eip-3529)。

### 4. 使用 indexed 事件

事件是一種輕量的紀錄機制，成本只是儲存操作的一小部分——基礎約 375 gas，每個 indexed 參數額外 375 gas，相較於寫入儲存空間所需的 20,000\+ gas。事件會被寫入交易收據樹（transaction receipt trie），這與合約狀態儲存空間是分開的，因此非常適合用來記錄鏈下應用需要追蹤的資訊。

關鍵限制在於：從合約的角度來看，事件是只寫的。一旦發出，你的合約程式碼就無法再讀回它們。事件的存在純粹是給前端、索引器與監控工具在外部使用的。請使用事件來處理需要通知的內容或鏈下系統需要的歷史記錄，但絕對不要用它來儲存合約邏輯依賴的資料。

這能卸載大量的 gas。與其把每一筆交易都儲存在成本高昂的儲存 array 中，不如發出一個事件，讓鏈下索引器（例如 [The Graph](https://www.alchemy.com/dapps/the-graph) 或 Alchemy 的 API）為你的前端建立那段歷史記錄。

以下是宣告與發出事件的方式：

<CodeSnippet language="solidity" code={`event MyFirstEvent(address indexed sender, uint256 indexed amount, string message);

function doSomething(uint256 \_amount, string memory \_message) public {
// Your contract logic here

    // Emit the event - cheap logging instead of expensive storage
    emit MyFirstEvent(msg.sender, _amount, _message);

}`} />

#### Indexed 參數

你最多可以將 3 個參數標記為 `indexed`，這會讓它們在 log 查詢中可被搜尋。舉例來說，將 `sender` 與 `amount` 設為 indexed 後，你就能快速過濾出「所有 sender = 0x123... 的事件」，而不用掃描每一個事件。像 `message` 這種未被 indexed 的參數，仍然會被記錄，但無法直接搜尋。

常見的使用案例包括代幣轉帳、擁有權變更、狀態轉換，以及使用者活動追蹤——任何你需要供鏈下使用的紀錄，但不需要鏈上存取的地方。想了解更多細節，可以參考 [Solidity 事件文件](https://docs.soliditylang.org/en/latest/contracts.html#events)。

### 5. 打包你的變數

EVM 以 32 位元組為單位儲存資料，每個儲存空間位置的寫入都要花費 gas（新位置需要 20,000\+ gas，更新則需要 2,900-5,000 gas）。透過策略性地將小型變數分組，你可以把多個變數塞進單一個儲存空間位置，大幅減少合約所需的 `SSTORE` 操作次數。

可以把這想成有效率地打包行李箱：你安排物品的順序，決定了你會浪費多少空間。變數會按照你宣告的順序被打包，因此謹慎安排相當重要。

之前（浪費了 3 個位置的空間）：

<CodeSnippet
  language="solidity"
  code={`contract MyContract {
    uint128 c; // Slot 0 (uses 16 bytes, wastes 16 bytes)
    uint256 b; // Slot 1 (uses full 32 bytes)
    uint128 a; // Slot 2 (uses 16 bytes, wastes 16 bytes)
}`}
/>

即使資料只需要 2.5 個位置的空間，這樣仍使用了 3 個儲存空間位置。

之後（壓縮進 2 個位置）：

<CodeSnippet
  language="solidity"
  code={`contract MyContract {
    uint128 a; // Slot 0 (first 16 bytes)
    uint128 c; // Slot 0 (second 16 bytes) - packed together!
    uint256 b; // Slot 1 (full 32 bytes)
}`}
/>

透過連續宣告兩個 `uint128` 變數，讓它們共用單一個位置，每次寫入這兩個變數時就能省下整整一次 `SSTORE` 操作。

打包的關鍵規則：

- 變數依宣告順序打包
- 當下一個變數無法容納在目前的位置中時，就會開始一個新的位置
- 較小的型別，例如 `uint8`、`uint128`、`address`（20 位元組），是打包的絕佳候選
- 即使一個小型別單獨存在，仍會佔用完整的 32 位元組位置，所以應盡量把它們配對

舉例來說，兩個 `address` 變數（各 20 位元組）無法打包進同一個位置，因為 40 位元組超過了 32 位元組。但一個 `address`（20 位元組）加上一個 `uint96`（12 位元組）就能完美塞進一個 32 位元組的位置。

想更了解 Solidity 如何安排儲存空間，可以參考[儲存布局文件](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html)。

### 6. 釋放未使用的儲存空間

當你將儲存變數清空回到預設值時（整數為 0、地址為 `address\(0\)`、布林值為 false），EVM 會提供 gas 退款。雖然 EIP-3529 已大幅減少這些退款的原始金額，但每清空一個儲存空間位置，你仍能拿回 4,800 gas：在清理過時資料時，這是一個值得注意的回收機會。

這是因為重設儲存空間位置能縮小區塊鏈的狀態大小，所以 Ethereum 會鼓勵這種清理行為。這是一種在資料變得過時後回收部分成本的方式，同時能讓你的合約狀態保持精簡有效率。

以下是清空變數的方式：

<CodeSnippet
  language="solidity"
  code={`delete myVariable; // Resets to default value and triggers refund// Or explicitly:
myInt = 0;
myAddress = address(0);
myBool = false;`}
/>

關於 mapping 的重要提醒：`delete` 關鍵字對整個 mapping 不起作用，因為 mapping 不會追蹤哪些鍵存在。你必須逐一刪除個別的 mapping 項目：

<CodeSnippet language="solidity" code={`mapping(address => uint256) public balances;

// This won't work - can't delete entire mapping// delete balances;// Instead, delete specific keys:
delete balances[msg.sender]; // Clears this specific entry`} />

#### 實務使用案例

Gas 退款在資料有明確生命週期的合約中最有用，例如可以在完成後清理的託管合約、會過期的臨時授權，或已經失效的快取資料。不要為了追求退款而扭曲你的合約邏輯，但當資料自然變得過時時，清理它是雙贏的做法。

想了解目前的退款機制與限制，可以參考 [EIP-3529 gas 退款文件](https://eips.ethereum.org/EIPS/eip-3529)。

### 7. 對特定函式參數使用 calldata 而非 memory 儲存資料

在宣告函式參數時，對於 array、字串、struct 這類參考型別，你可以在 `memory` 與 `calldata` 之間做選擇。理解兩者的差異能省下可觀的 gas，尤其是對於帶有大型參數的 external 函式而言。

**Calldata** 是唯讀儲存空間，直接存在於交易資料中。當你使用 `calldata` 時，函式會直接從交易中讀取參數，不需要複製到任何地方。這是最便宜的選項，因為它完全避免了記憶體配置與複製操作。

**Memory** 則需要 EVM 配置空間，並將資料從 calldata 複製到記憶體中，執行多次 `MLOAD` 與 `MSTORE` 操作。當處理大型 array 或字串時，這種複製開銷會變得很昂貴：每複製一個元素都要花費額外的 gas。

**規則**：對於你只需要讀取的 external 函式參數，使用 `calldata`。只有在你需要在函式內修改資料時，才使用 `memory`。

以下是使用 `calldata` 的範例（讀取專用時較便宜）：

<CodeSnippet
  language="solidity"
  code={`function processNumbers(uint[] calldata nums) external {
    for (uint i = 0; i < nums.length; i++) {
        // Read-only operations - no copy needed
        uint value = nums[i];
        // Do something with value
    }
}`}
/>

相比之下，`memory`（因為複製而較昂貴）：

<CodeSnippet
  language="solidity"
  code={`function processNumbers(uint[] memory nums) external {
    // Data gets copied from calldata to memory first (costs gas)
    for (uint i = 0; i < nums.length; i++) {
        uint value = nums[i];
    }
}`}
/>

Gas 節省的程度會隨資料大小增加，一個有 100 個元素的 array 用 `calldata` 傳遞，相較於 `memory`，可以省下數千 gas。

何時必須使用 memory：如果你的函式需要修改 array、對它進行附加，或建立新的資料結構，那就必須使用 `memory`，因為 `calldata` 是不可變的。但對於純讀取操作，`calldata` 永遠是更好的選擇。

想了解更多儲存位置之間的差異，可以參考 [calldata 與 memory 文件](https://docs.soliditylang.org/en/latest/types.html#data-location)。

### 8. 使用 immutable 與 constant 固定值

被標記為 `constant` 或 `immutable` 的變數完全不使用儲存空間位置：它們的值會在部署時直接嵌入合約的 bytecode 中。這消除了每次存取時昂貴的 `SLOAD` 操作（每次 2,100 gas），用便宜的 bytecode 讀取取而代之。

**Constant**：值必須在編譯時期設定，且無法更改。用於在所有部署中都不會改變的硬編碼值。

**Immutable**：值在建構函式中設定一次，之後就無法更改。用於在不同部署之間會有所不同（例如代幣地址或擁有者地址），但一旦部署後就固定不變的值。

以下是兩者的使用方式：

<CodeSnippet
  language="solidity"
  code={`contract MyContract {
    uint256 constant FEE_RATE = 10; // Set at compile time
    address immutable owner;        // Set once at deployment
    
    constructor(address _owner) {
        owner = _owner; // Can only set in constructor
    }
    
    function calculateFee(uint256 amount) public pure returns (uint256) {
        return amount * FEE_RATE / 100; // No SLOAD - reads from bytecode
    }
}`}
/>

Gas 節省範例：如果你在一個函式中讀取一個普通儲存變數 10 次，光是 `SLOAD` 操作就要花費 21,000 gas。使用 `constant` 或 `immutable`，這些讀取幾乎不花任何成本：只需要執行基本算術運算的 gas。

常見使用案例：

- 協議費率或百分比（`constant`）
- 數學常數，如小數位數或縮放係數（`constant`）
- 來自建構函式參數的代幣地址（`immutable`）
- 合約擁有者或管理員地址（`immutable`）
- 不會改變的外部合約地址（`immutable`）

`constant` 與 `immutable` 變數也都可以宣告在合約之外的檔案層級，讓它們可以在同一個檔案的多個合約中重複使用。

想了解更多細節，可以參考 [constant 與 immutable 文件](https://docs.soliditylang.org/en/latest/contracts.html#constant-and-immutable-state-variables)。

### 9. 使用 external 可見度修飾符

函式的可見度修飾符會影響 EVM 處理函式呼叫的方式，也會影響 gas 成本。關鍵差異在於：`external` 函式是專門針對合約外部呼叫優化的，而 `public` 函式必須同時處理外部與內部呼叫，因而增加了額外開銷。

**External 函式**只能從合約外部呼叫（透過交易或其他合約）。當從外部呼叫時，它們會直接從 calldata 讀取參數而不需複製，因此稍微更有效率。你無法用 `functionName\(\)` 在內部呼叫 external 函式——你需要使用 `this.functionName\(\)`，但這會產生一次昂貴的外部呼叫。

**Public 函式**可以同時被外部與內部呼叫。這種靈活性要求編譯器產生額外的程式碼來處理兩種呼叫類型，即使是從外部呼叫也會增加少量的 gas 開銷。

一般的經驗法則是：對於屬於你合約公開 API、只會被使用者或其他合約呼叫的函式，使用 `external`。對於只有你合約自己的函式才需要呼叫的輔助函式，使用 `internal` 或 `private`。

以下是 external 函式的範例（針對外部呼叫優化）：

<CodeSnippet
  language="solidity"
  code={`function updateMessage(string calldata _newMessage) external returns (string memory) {
    message = _newMessage;
    return message;
}`}
/>

相比之下，public（同時處理內部與外部）：

<CodeSnippet
  language="solidity"
  code={`function updateMessage(string memory _newMessage) public returns (string memory) {
    message = _newMessage;
    return message;
}`}
/>

Gas 節省幅度不大：通常每次呼叫大約 20-50 gas，但累積數千筆交易後就會累加起來。更重要的是，使用 `external` 能清楚表明意圖：這個函式是設計來從合約外部呼叫的。

額外提示：注意到 `external` 版本對字串參數使用了 `calldata` 嗎？External 函式與 calldata 參數是絕佳搭配，因為兩者都是針對外部呼叫優化的。

想了解更多函式可見度與最佳實務，可以參考[可見度文件](https://docs.soliditylang.org/en/latest/contracts.html#visibility-and-getters)。

### 10. 安全地使用 unchecked 算術

從 Solidity 0.8.0 開始，編譯器會自動為所有算術操作加上溢位與下溢檢查。雖然這能防止 bug，但每次操作大約要花費 30-40 gas。當你確定溢位/下溢在數學上不可能發生時，可以將操作包在 `unchecked` 區塊中，跳過這些檢查以節省 gas。

**何時安全**：邊界已知的迴圈計數器、已驗證輸入的算術運算，或者溢位在數學上不可能發生的操作。

**何時應避免**：未經驗證的使用者提供值、金融計算，或任何溢位可能造成漏洞的地方。

以下是一個基本範例：

<CodeSnippet
  language="solidity"
  code={`function add(uint x, uint y) external pure returns (uint) {
    unchecked {
        return x + y; // Skips overflow check - saves ~30 gas
    }
}`}
/>

以下是另一個展示迴圈計數器的範例，這是這項技巧最常見的使用案例：

<CodeSnippet
  language="solidity"
  code={`function processArray(uint[] calldata items) external {
    for (uint i = 0; i < items.length;) {
        // Process item
        
        unchecked {
            ++i; // i can never realistically overflow
        }
    }
}`}
/>

在這個迴圈中，`i` 從 0 開始，每次迭代加 1。若要讓 `i` 溢位，array 需要有 2^256 個元素，考量到區塊鏈的限制，這在物理上是不可能的。這正是 `unchecked` 的絕佳應用場景。

重要的安全提醒：如果你使用 `unchecked`，請加上手動的 `require` 陳述式，來驗證可能造成問題的輸入：

<CodeSnippet
  language="solidity"
  code={`function subtract(uint x, uint y) external pure returns (uint) {
    require(x >= y, "Underflow prevented");
    unchecked {
        return x - y; // Safe because validated above
    }
}`}
/>

在迴圈或頻繁呼叫的函式中，`unchecked` 區塊節省的 gas 會迅速累積。想了解更多細節與邊界情況，可以參考 [unchecked 文件](https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic)。

### 11. 減少外部呼叫

每次呼叫另一個合約，光是 `CALL` opcode 就要花費至少 100 gas，再加上 calldata 的額外成本，以及被呼叫合約中任何狀態變更的成本。如果外部合約發生 revert，這些呼叫也可能無法預測地失敗，讓它們既昂貴又有風險。當你需要來自外部合約的資料時，把多次呼叫打包在一起，或快取結果，以避免在同一筆交易中重複呼叫。

節省 gas 的做法：

<CodeSnippet language="solidity" code={`interface IExternal {
    function getData() external view returns (uint);
}

function processData(address addr) external {
// Call once and cache the result
uint data = IExternal(addr).getData();

    // Reuse cached value multiple times - no additional calls
    uint result1 = data * 2;
    uint result2 = data + 100;
    uint result3 = data / 5;

}`} />

沒效率的做法（應避免）：

<CodeSnippet
  language="solidity"
  code={`function processData(address addr) external {
    // Calling three separate times - wastes ~300 gas
    uint result1 = IExternal(addr).getData() * 2;
    uint result2 = IExternal(addr).getData() + 100;
    uint result3 = IExternal(addr).getData() / 5;
}`}
/>

如果你在一筆交易中需要多次使用相同的資料，一定要把它快取在本地變數中。如果你需要跨多筆交易使用外部資料，可以考慮把它儲存起來（不過要衡量 20,000 gas 的 `SSTORE` 成本相對於外部呼叫頻率是否划算）。

想了解外部呼叫的模式與安全考量，可以參考[外部呼叫最佳實務](https://docs.soliditylang.org/en/latest/security-considerations.html#use-the-checks-effects-interactions-pattern)。

### 12. 對關鍵路徑使用 assembly

對於效能關鍵的程式碼，例如密集迴圈或頻繁呼叫的函式，你可以降到 Yul assembly，手動優化到超越 Solidity 編譯器所能達到的程度。Assembly 讓你能直接控制記憶體管理、跳過安全檢查，並消除抽象層帶來的開銷。然而，這是一把雙面刃——assembly 會繞過 Solidity 所有的安全機制，讓程式碼更難閱讀，也極易出錯。

**何時考慮使用 assembly**：高頻率操作、複雜的位元操作、自訂的記憶體布局，或執行數千次、每一單位 gas 都很重要的迴圈。

**何時應避免使用 assembly**：除此之外的所有情況。省下的 gas 很少能彌補增加的 bug 風險、安全漏洞，以及維護負擔。

以下是使用 assembly 對 array 求和的範例：

<CodeSnippet
  language="solidity"
  code={`function sum(uint[] memory arr) public pure returns (uint s) {
    assembly {
        let len := mload(arr)                    // Load array length
        let data := add(arr, 0x20)               // Skip length, point to data
        
        for { let i := 0 } lt(i, len) { i := add(i, 1) } {
            s := add(s, mload(add(data, mul(i, 0x20))))  // Load and sum each element
        }
    }
}`}
/>

這個 assembly 版本透過直接操作記憶體指標並跳過邊界檢查來節省 gas，但相較於對等的 Solidity 程式碼，理解與審查的難度大幅提高。

#### 關鍵安全實務

- 徹底用邊界情況測試 assembly 程式碼
- 加上詳細的註解，說明每一個操作
- 讓 assembly 區段接受安全專家的審查
- 只在窮盡 Solidity 優化手段後，才把 assembly 作為最後手段

對大多數開發者與大多數使用案例來說，本指南中的其他 11 種技巧能以低得多的風險，帶來更好的 gas 節省效果。只有在你已經對合約做過效能分析、找出具體瓶頸，並確認節省的 gas 值得增加的複雜度後，才應該考慮使用 assembly。

想學習 Yul 語法與功能，可以參考 [Yul 文件](https://docs.soliditylang.org/en/latest/yul.html)。

## 部署前測試你的智慧合約

在部署到主網之前，使用開發工具嚴格測試你的 gas 用量。[Hardhat](https://hardhat.org/) 提供 gas reporter 外掛，能為每個函式產生詳細的 gas 成本分析。[Remix IDE](https://remix.ethereum.org/) 會在你測試時即時顯示 gas 估算值。將優化心力集中在面向使用者的函式上，例如鑄造、轉帳與兌換：這些是最常被呼叫的函式，對使用者體驗的影響也最大。

想進行更深入的測試，可以使用 [Foundry](https://book.getfoundry.sh/) 的模糊測試功能，在數千個隨機輸入上為你的優化進行基準測試，確保你節省的 gas 在真實情況與邊界案例下依然有效。

## 總結：開始優化吧

現在你已經有 12 種經過實戰驗證的技巧，能讓你的 Solidity 合約更精簡、執行成本更低。Gas 優化不只是省錢：它關乎為使用者打造更好的體驗，並創造出他們真正想要互動的應用。

先從容易上手的部分開始：啟用編譯器優化器、用 mapping 取代 array，並將固定值標記為 constant 或 immutable。接著對你的合約進行效能分析，找出瓶頸，再把更進階的技巧用在能發揮最大效果的地方。

**持續學習資源**：

- [Alchemy's Solidity Guides](https://www.alchemy.com/overviews/solidity-tutorial)
- [OpenZeppelin Standardized Contracts](https://docs.openzeppelin.com/contracts/)
- [Smart Contract Security Field Guide](https://scsfg.io/)

現在就開始動手優化吧，你的使用者（以及他們的錢包）會感謝你的！

## 常見問題

### 有哪些最有效的方法可以降低 Solidity 智慧合約的 gas 成本？

最有影響力的技巧包括：用 mapping 取代 array 進行查找、啟用 Solidity 編譯器優化器並設定合適的 runs 值、將固定值標記為 `constant` 或 `immutable`、減少儲存操作，以及對唯讀的函式參數使用 `calldata` 而非 `memory`。

### 使用 `constant` 與 `immutable` 變數對 gas 優化有什麼幫助？

`constant` 與 `immutable` 的值會直接嵌入合約 bytecode，而不是儲存在昂貴的儲存空間位置中，這消除了原本每次操作要花費的 2,100 gas `SLOAD` 操作，改用便宜的 bytecode 讀取取代。

### 為什麼在查找資料時應該使用 mapping 而非 array？

Mapping 無論資料量多寡，都能提供常數時間 O\(1\) 的查找，而 array 則需要 O\(n\) 的迭代，隨著 array 變大成本也會提高。對於以鍵為基礎的存取模式，例如使用者餘額或擁有權記錄，mapping 的成本明顯更低。

### Solidity 編譯器優化器如何運作？我應該使用什麼樣的 runs 設定？

優化器透過簡化表達式、移除無用程式碼，以及內聯函式來降低 gas。對於你會頻繁部署但很少呼叫的合約，使用低 runs（200）；對於交易量大、執行效率比部署成本更重要的合約，使用高 runs（10,000\+）。

### 在 gas 優化上，使用 `calldata`、`memory` 與 `storage` 有什麼差別？

對於你只需要讀取的 external 函式參數，使用 `calldata`（避免複製成本）；對於臨時性的資料操作，使用 `memory`；只有在需要持久狀態時，才使用 `storage`。對於唯讀操作，`calldata` 永遠比 `memory` 便宜。

### 我應該在什麼時候安全地使用 `unchecked` 算術區塊？

在溢位於數學上不可能發生的操作中使用 `unchecked`，例如邊界已知的迴圈計數器，或已驗證的算術運算。這能藉由跳過 Solidity 0.8.0 引入的自動溢位檢查，每次操作省下約 30-40 gas。

### 我要如何優化儲存操作以降低 gas 成本？

把多個小型變數打包進單一個 32 位元組的儲存空間位置、刪除未使用的儲存空間以取得 gas 退款（每清空一個位置可獲得 4,800 gas），並在函式執行期間把值快取在記憶體中，以減少儲存空間寫入。

### 使用 `external` 可見度修飾符相較於 `public` 有什麼好處？

`external` 函式是專門針對合約外部呼叫優化的，與 `calldata` 參數搭配使用效率很高，相較於必須同時處理內部與外部呼叫的 `public` 函式，每次呼叫可省下 20-50 gas。
