Audit, optimize, and maintain 60fps performance for emotive-mascot animations. Use when diagnosing performance issues, optimizing particle systems, reducing bundle size, or ensuring smooth animations across devices.
Diagnose and fix performance issues to maintain 60fps animations. Use when troubleshooting frame drops, optimizing particle counts, reducing bundle size, or investigating memory leaks across devices.
/plugin marketplace add joshtol/emotive-engine/plugin install joshtol-emotive-mascot-skills@joshtol/emotive-engineThis skill inherits all available tools. When active, it can use any tool Claude has access to.
You are an expert in optimizing the emotive-mascot engine for maximum performance across all devices.
const mascot = new EmotiveMascot({
containerId: 'mascot',
debug: true, // Enables FPS counter and debug overlay
});
// Access performance stats
mascot.getPerformanceStats();
// Returns:
// {
// fps: 60,
// frameTime: 16.67,
// particleCount: 800,
// memoryUsage: 45.2,
// renderTime: 12.3
// }
// Start profiling
performance.mark('mascot-start');
await mascot.transitionTo('joy', { duration: 1000 });
performance.mark('mascot-end');
performance.measure('mascot-transition', 'mascot-start', 'mascot-end');
const measures = performance.getEntriesByName('mascot-transition');
console.log('Transition took:', measures[0].duration, 'ms');
Symptoms: FPS < 60 on desktop, stuttering animations
Diagnosis:
// Check particle count
console.log(mascot.getCurrentParticleCount());
// Check render time
const stats = mascot.getPerformanceStats();
console.log('Render time:', stats.renderTime, 'ms');
Solutions:
Example fix:
// Before (laggy)
joy: {
particleCount: 1200,
trailLength: 15,
glow: true
}
// After (optimized)
joy: {
particleCount: 800,
trailLength: 5,
glow: false // or conditional based on device
}
Symptoms: Slow on mobile, high battery drain
Diagnosis:
// Detect mobile and adjust
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
const isLowEnd = navigator.hardwareConcurrency <= 4;
console.log('Mobile:', isMobile, 'Low-end:', isLowEnd);
Solutions:
Example fix:
const mascotConfig = {
containerId: 'mascot',
targetFPS: isMobile ? 30 : 60,
enableGazeTracking: !isMobile,
audioEnabled: !isLowEnd,
particleMultiplier: isMobile ? 0.6 : 1.0,
};
const mascot = new EmotiveMascot(mascotConfig);
Symptoms: Memory usage grows over time, page slows down
Diagnosis:
// Monitor memory over time
setInterval(() => {
if (performance.memory) {
console.log('Memory:', {
used:
(performance.memory.usedJSHeapSize / 1048576).toFixed(2) +
' MB',
total:
(performance.memory.totalJSHeapSize / 1048576).toFixed(2) +
' MB',
});
}
}, 5000);
Common causes:
Solutions:
// Proper cleanup in React
useEffect(() => {
const mascot = new EmotiveMascot({ containerId: 'mascot' });
mascot.initialize();
return () => {
mascot.destroy(); // Critical: cleanup on unmount
};
}, []);
// Manual cleanup
mascot.destroy(); // Removes listeners, stops animation, clears particles
Symptoms: Slow initial load, high bandwidth usage
Diagnosis:
# Check bundle size
npm run build
ls -lh dist/
# Analyze bundle composition
npm run build:analyze
Solutions:
Example fixes:
// Import only needed features
import { EmotiveMascot } from '@joshtol/emotive-engine/minimal';
// Dynamic import
const loadMascot = async () => {
const { EmotiveMascot } = await import('@joshtol/emotive-engine');
return new EmotiveMascot({ containerId: 'mascot' });
};
// Lazy load audio module
const loadAudio = async () => {
const { AudioEngine } = await import('@joshtol/emotive-engine/audio');
return new AudioEngine();
};
Instead of creating/destroying particles, reuse them:
// In PhysicsEngine.js
class ParticlePool {
constructor(maxSize = 1000) {
this.pool = [];
this.active = [];
this.maxSize = maxSize;
}
acquire() {
return this.pool.pop() || this.createParticle();
}
release(particle) {
if (this.pool.length < this.maxSize) {
particle.reset();
this.pool.push(particle);
}
}
}
Automatically adjust quality based on performance:
class AdaptiveQualityManager {
constructor(mascot) {
this.mascot = mascot;
this.targetFPS = 60;
this.currentFPS = 60;
this.checkInterval = 1000; // Check every second
}
monitor() {
setInterval(() => {
const stats = this.mascot.getPerformanceStats();
this.currentFPS = stats.fps;
if (this.currentFPS < this.targetFPS - 10) {
this.reduceQuality();
} else if (this.currentFPS >= this.targetFPS) {
this.increaseQuality();
}
}, this.checkInterval);
}
reduceQuality() {
// Reduce particle count by 20%
this.mascot.setParticleMultiplier(0.8);
// Disable trails
this.mascot.setTrailsEnabled(false);
}
increaseQuality() {
// Restore particle count
this.mascot.setParticleMultiplier(1.0);
// Enable trails
this.mascot.setTrailsEnabled(true);
}
}
Minimize canvas operations:
// Batch operations
ctx.save();
// ... multiple operations
ctx.restore();
// Use transforms instead of recalculating
ctx.translate(x, y);
ctx.rotate(angle);
// ... draw
ctx.resetTransform();
// Avoid unnecessary state changes
const prevFillStyle = ctx.fillStyle;
if (prevFillStyle !== newColor) {
ctx.fillStyle = newColor;
}
Reduce physics calculations:
// Spatial partitioning for collision detection
class SpatialGrid {
constructor(cellSize = 50) {
this.cellSize = cellSize;
this.grid = new Map();
}
insert(particle) {
const cellX = Math.floor(particle.x / this.cellSize);
const cellY = Math.floor(particle.y / this.cellSize);
const key = `${cellX},${cellY}`;
if (!this.grid.has(key)) {
this.grid.set(key, []);
}
this.grid.get(key).push(particle);
}
getNearby(particle) {
const cellX = Math.floor(particle.x / this.cellSize);
const cellY = Math.floor(particle.y / this.cellSize);
const nearby = [];
// Check 3x3 grid around particle
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const key = `${cellX + dx},${cellY + dy}`;
if (this.grid.has(key)) {
nearby.push(...this.grid.get(key));
}
}
}
return nearby;
}
}
debug: true to monitor FPS// Add to demo pages for quick profiling
class PerformanceProfiler {
constructor(mascot) {
this.mascot = mascot;
this.results = [];
}
async profileEmotion(emotion, duration = 5000) {
console.log(`Profiling ${emotion}...`);
const startMem = performance.memory?.usedJSHeapSize || 0;
const startTime = performance.now();
let frameCount = 0;
let totalFrameTime = 0;
const measureFrame = () => {
const frameStart = performance.now();
frameCount++;
const frameTime = performance.now() - frameStart;
totalFrameTime += frameTime;
};
const interval = setInterval(measureFrame, 16);
await this.mascot.transitionTo(emotion);
await new Promise(resolve => setTimeout(resolve, duration));
clearInterval(interval);
const endTime = performance.now();
const endMem = performance.memory?.usedJSHeapSize || 0;
const result = {
emotion,
avgFPS: frameCount / (duration / 1000),
avgFrameTime: totalFrameTime / frameCount,
memoryDelta: (endMem - startMem) / 1048576,
totalTime: endTime - startTime,
};
this.results.push(result);
console.table(result);
return result;
}
async profileAll(emotions = ['calm', 'joy', 'excitement', 'focus']) {
for (const emotion of emotions) {
await this.profileEmotion(emotion);
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log('=== PERFORMANCE SUMMARY ===');
console.table(this.results);
return this.results;
}
}
// Usage:
const profiler = new PerformanceProfiler(mascot);
await profiler.profileAll();
{
"emotive-mascot.umd.js": "900 KB uncompressed, 234 KB gzipped",
"emotive-mascot.minimal.js": "400 KB uncompressed, 120 KB gzipped",
"emotive-mascot.audio.js": "700 KB uncompressed, 200 KB gzipped"
}
// rollup.config.js optimizations
import terser from '@rollup/plugin-terser';
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
terser({
compress: {
drop_console: true, // Remove console.logs in production
drop_debugger: true,
pure_funcs: ['console.log', 'console.debug'],
},
mangle: {
properties: {
regex: /^_/, // Mangle private properties
},
},
}),
visualizer({
filename: 'bundle-analysis.html',
open: true,
}),
],
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
},
};
src/core/PerformanceMonitor.jssrc/core/PhysicsEngine.jssrc/core/ParticleSystem.jssrc/core/RenderEngine.jsrollup.config.jspackage.json (bundlesize settings)This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.