Real-world approaches to gas optimization
Gas optimization is an art. Small changes can save users significant money, but premature optimization can make code unreadable and bug-prone. Here's how we approach it.
Start by profiling. Use tools like Foundry's gas reports to understand where gas is actually being spent. Often, the expensive parts aren't where you'd expect. Storage operations, loops, and external calls are the usual culprits.
Storage is expensive—21,000 gas for a new slot, 5,000 for updates. Pack your variables (multiple uint8s in one slot), use mappings instead of arrays when possible, and cache storage reads in memory variables.
Calldata vs memory matters for function parameters. Use calldata for read-only array parameters—it's cheaper than memory. For structs that you're only reading, calldata saves significant gas.
Loops are dangerous. Unbounded loops can make your contract unusable if gas costs exceed block limits. If you need to iterate over large datasets, consider patterns like pagination or off-chain computation with on-chain verification.
Assembly can save gas but use it sparingly. It's hard to audit and easy to introduce bugs. Common safe patterns: custom errors, efficient keccak256 hashing, and optimized memory operations. For anything complex, stick with Solidity.
Don't forget about deployment costs. Longer bytecode costs more to deploy. Remove unused functions, use libraries for repeated code, and consider proxy patterns for large contracts.
Key Takeaways
- Profile before optimizing—find the real bottlenecks
- Pack storage variables and cache reads
- Use calldata for read-only parameters
- Avoid unbounded loops, use pagination patterns
- Assembly is powerful but risky—use sparingly