DEV Community

Abdullah Developer
Abdullah Developer

Posted on

Building a Game Utility Tool for 100K+ Players: Technical Breakdown & Lessons Learned

Introduction

I built Grow a Garden Calculators because I got tired of doing mental math to figure out crop values in a Roblox game. What started as a personal side project turned into a tool used by thousands of players.

This post isn't about the game itself. It's about the technical decisions I made to build a tool that needed to be:

Fast (instant calculations, no lag)
Reliable (thousands of concurrent users)
Scalable (handling traffic spikes when the game updates)
Lightweight (browser-based, no downloads)

If you've ever built a utility tool, you'll recognize these problems.

The Problem

Players needed to know crop values instantly. The game doesn't provide this data, so players were doing manual calculations or using outdated spreadsheets.

Requirements:

Calculate crop values based on 5+ variables (crop type, weight, mutations)
Handle edge cases (different mutation combinations)
Work on mobile and desktop
No backend dependency (cheaper, faster, more reliable)
Instant results (< 100ms response time)
Architecture Decision: Frontend-Only

I could have built this as:

Backend + Database - Overkill, slower, more expensive
Static HTML/JS - Too limited
Progressive Web App - Local storage + client-side logic

I chose PWA with client-side calculations because:

✅ No server costs (GitHub Pages or free hosting)
✅ Instant calculations (all data in browser)
✅ Works offline after first load
✅ No database delays
✅ User data stays private (no logging)

The Tech Stack
Frontend: HTML5 + Vanilla JavaScript (no frameworks)
Storage: LocalStorage for user preferences
Hosting: GitHub Pages (free, fast, reliable)
Version Control: Git (obviously)

Why vanilla JS instead of React/Vue?

This tool doesn't need:

Complex state management
Component reusability
Virtual DOM (overkill for a calculator)

Vanilla JS meant:

Smaller bundle size (30KB vs 200KB+)
Faster load times
Zero framework overhead
Easier for contributors
Data Structure

The core of the tool is a clean data structure for crop values:

javascript
const crops = {
watermelon: {
baseValue: 500,
weight: { min: 1, max: 5 },
mutations: {
rare: 1.5,
legendary: 2.5,
exotic: 3.0
}
},
pumpkin: {
baseValue: 450,
weight: { min: 1, max: 4 },
mutations: {
rare: 1.6,
legendary: 2.8,
exotic: 3.2
}
},
// ... more crops
};

function calculateValue(crop, weight, mutations) {
let value = crops[crop].baseValue;

// Apply weight multiplier
value *= (weight / 2); // Normalized to 2kg baseline

// Apply mutation multipliers (stack them)
mutations.forEach(mutation => {
value *= crops[crop].mutations[mutation];
});

return Math.round(value);
}

This approach:

✅ O(1) lookups
✅ Easy to update (just change the JSON)
✅ Transparent (players can verify math)
✅ No API calls needed
Performance Optimization

Initial problem: Calculations were instant, but the DOM updates were lagging on mobile.

Solution: Debouncing

javascript
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

const debouncedCalculate = debounce(calculateAndDisplay, 100);

// On user input
cropInput.addEventListener('input', debouncedCalculate);

Results:

Before: 16ms-50ms lag when typing quickly
After: Smooth 60fps even on cheap phones

Why debounce matters:

Users were getting frustrated with lag
Mobile phones have limited resources
Unnecessary recalculations waste battery
Handling Edge Cases

Challenge: Players would input invalid values. The app crashed on some browsers.

Solutions implemented:

javascript
function validateInput(value, type) {
// Crop must exist
if (!crops[value]) return false;

// Weight must be reasonable
if (type === 'weight' && (value < 0.5 || value > 10)) return false;

// Mutations must be valid
if (type === 'mutation' && !validMutations.includes(value)) return false;

return true;
}

// Sanitize all inputs
document.getElementById('input').addEventListener('change', (e) => {
if (!validateInput(e.target.value, 'crop')) {
e.target.value = 'watermelon'; // Reset to default
showError('Invalid crop selected');
}
});

Result: Zero crashes from invalid input. Better UX.

User Preferences with LocalStorage

Players wanted their calculator to "remember" their favorite crops.

javascript
const preferences = {
lastCrop: 'watermelon',
lastWeight: 2.5,
favoritesCrops: ['watermelon', 'pumpkin']
};

// Save
localStorage.setItem('calc-prefs', JSON.stringify(preferences));

// Load on startup
function loadPreferences() {
const saved = localStorage.getItem('calc-prefs');
return saved ? JSON.parse(saved) : defaultPrefs;
}

Impact:

Users don't have to reselect crops every time
App feels faster (instant load)
Better retention
Analytics Without Tracking

I wanted to know what players were calculating, but couldn't track them (privacy).

Solution: Anonymous aggregate data

javascript
// Count which crops are calculated (no personal data)
function trackCalculation(crop) {
let stats = JSON.parse(localStorage.getItem('calc-stats') || '{}');
stats[crop] = (stats[crop] || 0) + 1;
localStorage.setItem('calc-stats', JSON.stringify(stats));
}

This tells me:

"Watermelons are calculated 10x more than carrots"
Which crops to prioritize updating
No IP addresses, no personal data, fully compliant with privacy
Deployment & Hosting

GitHub Pages (free, reliable, instant):

Push to main branch
Auto-deploys in 60 seconds
CDN gives users instant load times
No server maintenance
bash
git add .
git commit -m "Update crop values for game patch"
git push origin main

Deployed. Done.

Cost: $0/month
Uptime: 99.9%+
Load time: <500ms globally

Handling Traffic Spikes

When Roblox pushed a major game update, I got 10K users in 1 day.

Problems:

GitHub Pages wasn't designed for that traffic
But it still worked perfectly

Why:

Static HTML loads from CDN
No database queries
No server processing
Each user's calculation runs locally

The tool scaled to 100K+ monthly users with zero changes needed.

What I'd Do Differently

Mistakes I made:

Hardcoded data initially - Should have structured it sooner
Fix: JSON-based config from day 1
No version history - Players couldn't track game updates
Fix: Add "Last Updated" date, changelog
Mobile viewport issues early on - Tested only on desktop
Fix: Mobile-first development from the start
No error boundaries - One bad input broke the whole app
Fix: Defensive programming matters
Lessons Learned

Technical:

Vanilla JS is fine for tools (doesn't need React)
Client-side calculations = unlimited scalability
LocalStorage is powerful for simple state
Debouncing fixes 90% of "lag" complaints

Business:

Build for a specific problem (not "everything")
Users find you if it solves their exact pain
Free + useful = word-of-mouth growth
Tool usage: 100K+ players, zero marketing spend

Community:

Document why you made decisions
GitHub repos with good READMEs get stars
Open source tools compound over time
How to Build Your Own Tool

If you want to build a utility like this:

Pick a specific problem (not "make a calculator")
Choose vanilla JS first (add frameworks only if needed)
Deploy free (GitHub Pages, Netlify, Vercel)
Share with niche communities (where your users are)
Listen to feedback (players tell you what they need)
Open to Feedback

If you're interested in the actual codebase, I'd be open to open-sourcing parts of it. The game community benefits from transparency.

"Try the calculator: https://growsagardencalculators.com/
Questions? Drop them in the comments. I read every one.

Tags: #javascript #webdev #gaming #performance #indiedev #tools

Top comments (0)