Post
Share your knowledge.
Gas Optimization in Dynamic NFT Metadata Updates
How can I optimize gas usage in a Sui smart contract when batching Move transactions that modify shared objects, especially when handling dynamic NFT metadata updates?
- Sui
- Architecture
- SDKs and Developer Tools
- NFT Ecosystem
- Move
Answers
2I’ve had to carefully consider gas optimization—especially when batching transactions that modify shared objects like dynamic NFTs.
Gas costs can escalate quickly in Sui when multiple shared objects are mutated in one transaction. Here's how I typically optimize gas usage when handling batched updates to dynamic NFT metadata.
🧠 Core Optimization Strategies
- Minimize Object Mutations: Only mutate what’s strictly necessary.
- Use Event Emission for Logs: Instead of storing a full history on-chain.
- Batch Using a Controller Object: Use a single shared object as a controller to mutate other child objects.
- Avoid Repeated Borrowing of Shared Objects: This adds gas due to dependency resolution.
- Use
option<T>and defaulting over vector pushes: More gas-efficient.
💡 Use Case: Batched Metadata Update for Dynamic NFTs
Let’s say I want to update metadata (like levels, scores, traits) on multiple NFTs owned by a user. Here’s how I design it:
🧱 1. Shared Object: MetadataController
This acts as a manager for metadata updates. Only it gets mutated in batched calls.
module example::metadata_controller {
use sui::object::UID;
use sui::tx_context::TxContext;
use sui::event;
use sui::option::{Self, Option};
struct Metadata has key {
level: u64,
score: u64,
name: vector<u8>,
}
struct MetadataController has key {
id: UID,
// Mapping NFT ID to Metadata
metadata_store: table::Table<address, Metadata>,
}
public fun create(ctx: &mut TxContext): MetadataController {
MetadataController {
id: object::new(ctx),
metadata_store: table::new<address, Metadata>(ctx),
}
}
public fun batch_update(
controller: &mut MetadataController,
nft_ids: vector<address>,
levels: vector<u64>,
scores: vector<u64>,
names: vector<vector<u8>>,
) {
let len = vector::length(&nft_ids);
assert!(
len == vector::length(&levels)
&& len == vector::length(&scores)
&& len == vector::length(&names),
0
);
let i = 0;
while (i < len) {
let nft = *vector::borrow(&nft_ids, i);
let metadata = Metadata {
level: *vector::borrow(&levels, i),
score: *vector::borrow(&scores, i),
name: vector::borrow(&names, i),
};
table::insert(&mut controller.metadata_store, nft, metadata);
i = i + 1;
}
event::emit(ctx, BatchUpdated {
updated_count: len as u64
});
}
struct BatchUpdated has copy, drop, store {
updated_count: u64,
}
}
🛠️ 2. Client-Side Batching
On the frontend or CLI, I prepare the batched call once, so Sui can resolve dependencies efficiently:
// pseudo-code using Sui JS SDK
const tx = new TransactionBlock();
tx.moveCall({
target: "example::metadata_controller::batch_update",
arguments: [
tx.object(controllerId), // shared object
tx.pure(nftAddresses), // vector<address>
tx.pure(levels), // vector<u64>
tx.pure(scores), // vector<u64>
tx.pure(names), // vector<vector<u8>>
],
});
let result = await suiProvider.signAndExecuteTransactionBlock({
transactionBlock: tx,
signer: wallet,
});
📉 Why This Is Gas-Efficient
- Only one shared object (
controller) is being mutated. - All NFT metadata is stored in a central table, avoiding multiple object references.
- You avoid expensive
&mutaccess to each NFT shared object individually. - Using vectors and loop logic in Move is cheaper than multiple separate calls.
🧪 Optional: Off-Chain Indexing
Since the metadata is stored under a controller, indexers (like Suiet or custom infra) can extract and associate it off-chain to user-facing NFTs. This saves gas and keeps the user experience rich.
✅ Summary Checklist
| Optimization | Applied |
|---|---|
| Single shared object mutation | ✅ |
| Batch update with vectors | ✅ |
| Avoided redundant borrows | ✅ |
| Used table for key-value | ✅ |
| Event used instead of logs | ✅ |
If you're working with on-chain metadata updates for a large NFT set (e.g., in-game characters, evolving collectibles), this pattern works really well. Let me know if you want a version where each NFT is a separate shared object—there are tricks for that too, but it gets more expensive.
To reduce gas costs when batching Move transactions that update shared objects like dynamic NFT metadata in a Sui smart contract, you should minimize unnecessary state changes and structure your logic to avoid repeated reads and writes. One key approach is to group metadata updates efficiently—only touch fields that actually change and avoid full object rewrites. You can also use event-driven patterns or off-chain triggers to determine when updates are needed, instead of updating on every interaction. When batching, combine multiple metadata updates into a single transaction block where possible, which reduces overhead per operation. Also, leverage Sui’s object model smartly: design your NFT metadata as separate, modular objects that can be updated independently instead of embedding everything into one large object. Finally, reusing object references and optimizing access patterns in Move code (like borrowing instead of copying objects) will keep gas usage low during execution.
Read more: https://docs.sui.io/build/gas
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410