Near Learn

Load only what a call needs

Keep NEAR contract root state small to save gas: LazyOption for big values, store collections that load per key, and caching reads within a single call.

Advanced7 min read3-question check

Every call into your contract starts the same way: near-sdk reads the root state record and Borsh-deserializes the whole contract struct. Every change call (&mut self) ends the same way: the struct is serialized and written back. The size of that struct is a tax on every method, including the cheap ones.

So the goal is a small root struct whose fields are mostly handles, with the real data in separate storage keys that a call loads only when it touches them.

Keep the root struct small#

Big, rarely used data goes behind a LazyOption
Rust
use near_sdk::json_types::U128;
use near_sdk::store::{LazyOption, LookupMap};
use near_sdk::{near, AccountId, BorshStorageKey, PanicOnDefault};

#[near(serializers = [borsh, json])]
#[derive(Clone)]
pub struct Metadata {
    name: String,
    description: String,  // can be kilobytes
    icon: Option<String>, // a data: URL, often bigger still
}

#[near(serializers = [borsh])]
#[derive(BorshStorageKey)]
enum StorageKey {
    Metadata,
    Balances,
}

#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Token {
    owner: AccountId,
    total_supply: u128,
    metadata: LazyOption<Metadata>,         // own storage key, loaded on access
    balances: LookupMap<AccountId, u128>,   // one entry per key, loaded on access
}

#[near]
impl Token {
    #[init]
    pub fn new(owner: AccountId, metadata: Metadata) -> Self {
        Self {
            owner,
            total_supply: 0,
            metadata: LazyOption::new(StorageKey::Metadata, Some(metadata)),
            balances: LookupMap::new(StorageKey::Balances),
        }
    }

    // never pays to load the metadata
    pub fn balance_of(&self, account_id: AccountId) -> U128 {
        U128(self.balances.get(&account_id).copied().unwrap_or(0))
    }

    pub fn metadata(&self) -> Option<Metadata> {
        self.metadata.get().clone()
    }
}

With this layout the root record holds an account ID, a u128 and two short prefixes. balance_of and any transfer method never deserialize the metadata; only metadata() reads it. A LazyOption stores its value under its own key, loads it on first access, and keeps it cached for the rest of the call.

Use store::Lazy<T> for the same trick when the value is always present, and store collections for anything that grows with the number of users.

Cache within a call#

One lookup, edited in place, written once at the end of the call
Rust
// same Token contract, plus: use near_sdk::{env, require};
#[near]
impl Token {
    pub fn credit(&mut self, account_id: AccountId, amount: U128) {
        require!(env::predecessor_account_id() == self.owner, "owner only");
        let balance = self.balances.entry(account_id).or_insert(0);
        *balance += amount.0;
        self.total_supply += amount.0;
    }
}

near_sdk::store collections cache what they read and buffer what they write, so reading the same key twice in one call does not hit storage twice, and repeated updates to one key become a single write at the end. The legacy near_sdk::collections types do not cache: every get is a fresh storage read and deserialization. Either way, the clearest habit is the same: read a value once, work on it locally, write it once.

Check yourself

3 questions · progress saved in this browser

  1. 1.Which data is read and deserialized on every call to a near-sdk contract, whatever the method?
  2. 2.You move a 5 KB metadata struct from an inline field into a LazyOption. What changes?
  3. 3.A method only reads state but is declared with &mut self. What is the cost?