Leaderboard Best Practices
This guide covers best practices for implementing efficient and engaging leaderboard systems with Moitribe JavaScript SDK.
Performance Optimization
Caching Strategy
Implement intelligent caching to reduce API calls and improve user experience:
class LeaderboardCache {
constructor() {
this.cache = new Map();
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}
set(key, data) {
this.cache.set(key, {
data: data,
timestamp: Date.now()
});
}
get(key) {
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > this.cacheTimeout) {
this.cache.delete(key);
return null;
}
return cached.data;
}
clear() {
this.cache.clear();
}
}
const leaderboardCache = new LeaderboardCache();
// Cached leaderboard loading
function loadLeaderboardScores(leaderboardId, collection = 1, timespan = 0) {
const cacheKey = `${leaderboardId}_${collection}_${timespan}`;
const cached = leaderboardCache.get(cacheKey);
if (cached) {
console.log('Loading from cache...');
return Promise.resolve(cached);
}
return new Promise((resolve) => {
MoitribeSDK(GAME_ID, 'loadleaderboardtopscores', {
leaderboardid: leaderboardId,
collection: collection,
timespan: timespan,
maxresults: 25,
onlyData: true,
callback: function(result) {
if (result.success && result.scores) {
leaderboardCache.set(cacheKey, result.scores);
resolve(result.scores);
} else {
resolve([]);
}
}
});
});
}
Batch Operations
Group multiple operations to reduce network overhead:
class LeaderboardBatcher {
constructor() {
this.pending = [];
this.batchTimeout = 100; // 100ms
this.timer = null;
}
add(operation) {
this.pending.push(operation);
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.batchTimeout);
}
}
flush() {
if (this.pending.length === 0) return;
const operations = [...this.pending];
this.pending = [];
this.timer = null;
// Process batched operations
this.processBatch(operations);
}
async processBatch(operations) {
// Group by leaderboard ID
const grouped = operations.reduce((acc, op) => {
if (!acc[op.leaderboardId]) acc[op.leaderboardId] = [];
acc[op.leaderboardId].push(op);
return acc;
}, {});
// Process each group
for (const [leaderboardId, ops] of Object.entries(grouped)) {
await this.processLeaderboardGroup(leaderboardId, ops);
}
}
async processLeaderboardGroup(leaderboardId, operations) {
// Get metadata once for the group
const metadata = await this.getMetadata(leaderboardId);
// Process all operations for this leaderboard
for (const op of operations) {
switch (op.type) {
case 'submit':
await this.submitScore(leaderboardId, op.score, op.tag);
break;
case 'load':
await this.loadScores(leaderboardId, op.collection, op.timespan);
break;
}
}
}
}
const batcher = new LeaderboardBatcher();
// Use batcher for multiple operations
function submitMultipleScores(scores) {
scores.forEach(score => {
batcher.add({
type: 'submit',
leaderboardId: score.leaderboardId,
score: score.value,
tag: score.tag
});
});
}
User Experience
Progressive Loading
Load data progressively to show content quickly:
class ProgressiveLeaderboardLoader {
constructor() {
this.currentPage = 0;
this.pageSize = 25;
this.isLoading = false;
this.hasMore = true;
}
async loadMore(leaderboardId, collection = 1, timespan = 0) {
if (this.isLoading || !this.hasMore) return;
this.isLoading = true;
this.updateLoadingUI(true);
try {
const newScores = await this.loadPage(leaderboardId, collection, timespan, this.currentPage);
if (newScores.length < this.pageSize) {
this.hasMore = false;
}
this.currentPage++;
this.appendScores(newScores);
this.updateLoadMoreButton(this.hasMore);
} catch (error) {
console.error('Failed to load more scores:', error);
this.showError('Failed to load more scores');
} finally {
this.isLoading = false;
this.updateLoadingUI(false);
}
}
loadPage(leaderboardId, collection, timespan, page) {
return new Promise((resolve) => {
MoitribeSDK(GAME_ID, 'loadleaderboardtopscores', {
leaderboardid: leaderboardId,
collection: collection,
timespan: timespan,
maxresults: this.pageSize,
offset: page * this.pageSize,
onlyData: true,
callback: function(result) {
resolve(result.success ? result.scores : []);
}
});
});
}
updateLoadingUI(isLoading) {
const loadingElement = document.getElementById('leaderboard-loading');
if (loadingElement) {
loadingElement.style.display = isLoading ? 'block' : 'none';
}
}
updateLoadMoreButton(hasMore) {
const button = document.getElementById('load-more-button');
if (button) {
button.style.display = hasMore ? 'block' : 'none';
}
}
appendScores(scores) {
const container = document.getElementById('leaderboard-scores');
if (!container) return;
scores.forEach(score => {
const scoreElement = this.createScoreElement(score);
container.appendChild(scoreElement);
});
}
createScoreElement(score) {
const div = document.createElement('div');
div.className = 'leaderboard-score';
div.innerHTML = `
<div class="rank">${score.rank || '-'}</div>
<div class="player">${score.playerName || 'Anonymous'}</div>
<div class="score">${this.formatScore(score.score)}</div>
<div class="date">${this.formatDate(score.timestamp)}</div>
`;
return div;
}
formatScore(score) {
if (score >= 1000000) {
return (score / 1000000).toFixed(1) + 'M';
} else if (score >= 1000) {
return (score / 1000).toFixed(1) + 'K';
}
return score.toString();
}
formatDate(timestamp) {
return new Date(timestamp).toLocaleDateString();
}
showError(message) {
const errorElement = document.getElementById('leaderboard-error');
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}
}
}