Skip to content

Max Particles

The maxParticles configuration option is your primary tool for balancing visual quality with performance. It sets a hard limit on the number of particles that can be created, directly impacting frame rate, CPU usage, and visual density. Understanding how to use this option effectively is crucial for creating performant particle text effects across all devices.

maxParticles caps the total number of particles rendered on the canvas. When the particle generation process reaches this limit, it stops creating new particles, even if the text rendering could produce more.

const instance = initParticleJS('#canvas', {
text: 'PERFORMANCE',
colors: ['#695aa6'],
maxParticles: 2000 // Limit to 2000 particles
});
// Without maxParticles, complex text might generate 8000+ particles
// With maxParticles: 2000, it stops at 2000
// Result: 3-4x better performance, slightly less detail
// Default maxParticles value
const instance = initParticleJS('#canvas', {
text: 'DEFAULT',
colors: ['#695aa6']
// maxParticles: 5000 (default)
});
// Explicitly set to a different value
const instance = initParticleJS('#canvas', {
text: 'CUSTOM',
colors: ['#695aa6'],
maxParticles: 1500 // Override default
});

Default: 5000 particles

This default balances quality and performance for modern desktop browsers. For most use cases, you’ll want to override this based on your specific needs.

// Simplified particle generation logic
function generateParticles(text, maxParticles) {
const particles = [];
// Draw text to hidden canvas
context.fillText(text, x, y);
// Sample pixels to create particles
for (let y = 0; y < height; y += samplingRate) {
for (let x = 0; x < width; x += samplingRate) {
const pixel = context.getImageData(x, y, 1, 1).data;
if (pixel[3] > 128) { // If pixel is opaque enough
particles.push(createParticle(x, y));
// Stop when limit reached
if (particles.length >= maxParticles) {
return particles; // Early exit
}
}
}
}
return particles;
}

Key behaviors:

  1. Particles are created by sampling pixels from rendered text
  2. Generation stops immediately when maxParticles is reached
  3. Lower values = faster initialization
  4. Result: predictable performance regardless of text complexity
// Low particle count (500-1000)
// CPU: ~5-10% on modern desktop
// Mobile: ~15-20%
// Frame rate: Consistent 60 FPS
const lowPerf = initParticleJS('#canvas', {
text: 'LOW',
colors: ['#695aa6'],
maxParticles: 800
});
// Medium particle count (1500-2500)
// CPU: ~15-25% on modern desktop
// Mobile: ~30-40%
// Frame rate: 50-60 FPS desktop, 30-50 FPS mobile
const medPerf = initParticleJS('#canvas', {
text: 'MEDIUM',
colors: ['#695aa6'],
maxParticles: 2000
});
// High particle count (4000-5000)
// CPU: ~30-50% on modern desktop
// Mobile: 50-80% (may struggle)
// Frame rate: 40-60 FPS desktop, 20-40 FPS mobile
const highPerf = initParticleJS('#canvas', {
text: 'HIGH',
colors: ['#695aa6'],
maxParticles: 5000
});
Particle CountDesktop CPUMobile CPUDesktop FPSMobile FPSInitialization
5005-10%15-20%6060<50ms
100010-15%20-30%6050-6050-100ms
200015-25%30-40%50-6030-50100-200ms
300020-35%40-60%45-5525-40150-300ms
500030-50%50-80%40-6020-40250-500ms

Tested on: Desktop (M1 MacBook Pro), Mobile (iPhone 12)

// Sparse particles: Fast but less detailed
const sparse = initParticleJS('#canvas', {
text: 'SPARSE',
colors: ['#695aa6'],
maxParticles: 500,
fontSize: 80
});
// Result: Clear letters but grainy texture
// Good for: Subtle backgrounds, decorative elements
// Balanced particles: Good detail and performance
const balanced = initParticleJS('#canvas', {
text: 'BALANCED',
colors: ['#695aa6'],
maxParticles: 2000,
fontSize: 80
});
// Result: Smooth text with good readability
// Good for: Hero sections, main content
// Dense particles: Maximum detail but slower
const dense = initParticleJS('#canvas', {
text: 'DENSE',
colors: ['#695aa6'],
maxParticles: 5000,
fontSize: 80
});
// Result: Crisp, detailed text
// Good for: Showcases, landing pages (desktop only)

You can maintain visual quality with fewer particles:

// Strategy 1: Increase particle size
const instance = initParticleJS('#canvas', {
text: 'LARGER',
colors: ['#695aa6'],
maxParticles: 1000, // Lower count
particleRadius: {
xs: { base: 3, rand: 1 }, // Larger particles (default: 2)
lg: { base: 3, rand: 1 }
}
});
// Fewer particles but they cover more area
// Result: Smooth appearance, better performance
// Strategy 2: Increase font size
const instance2 = initParticleJS('#canvas', {
text: 'BIG',
colors: ['#695aa6'],
maxParticles: 1200,
fontSize: 120 // Larger text (default: 80)
});
// Particles spread over larger area
// Result: Better particle density perception
// Strategy 3: Shorter text
const instance3 = initParticleJS('#canvas', {
text: 'OK', // Short text (2 characters vs 8)
colors: ['#695aa6'],
maxParticles: 1000
});
// Same particle budget for fewer letters
// Result: Very dense, high-quality rendering
// Detect mobile device
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const instance = initParticleJS('#canvas', {
text: 'MOBILE',
colors: ['#695aa6'],
maxParticles: isMobile ? 1000 : 3000, // 1/3 particles on mobile
// Also increase particle size on mobile
particleRadius: isMobile ? {
xs: { base: 3, rand: 1 }
} : {
xs: { base: 2, rand: 1 }
}
});
// Recommended mobile ranges:
// - Background effects: 500-800
// - Hero sections: 1000-1500
// - Showcase (if needed): 1500-2000 max
// Detect low-end devices
const isLowEnd = /iPhone [1-7]|Android [1-5]|iPad [1-4]/i.test(navigator.userAgent);
const instance = initParticleJS('#canvas', {
text: 'LOWEND',
colors: ['#695aa6'],
maxParticles: isLowEnd ? 500 : 2500,
// Enable slow browser detection
renderTimeThreshold: isLowEnd ? 8 : 16,
// Larger particles
particleRadius: {
xs: { base: isLowEnd ? 4 : 2, rand: 1 }
}
});
// Low-end device recommendations:
// - Background: 300-500
// - Hero: 500-800
// - Avoid showcases on low-end devices
// Detect high-end desktop
const isHighEnd = window.innerWidth >= 1920 &&
!(/iPhone|iPad|iPod|Android/i.test(navigator.userAgent));
const instance = initParticleJS('#canvas', {
text: 'DESKTOP',
colors: ['#695aa6'],
maxParticles: isHighEnd ? 5000 : 3000,
fontSize: isHighEnd ? 120 : 80,
explosionRadius: {
xs: isHighEnd ? 100 : 80,
lg: isHighEnd ? 150 : 120
}
});
// Desktop recommendations:
// - Background: 1500-2500
// - Hero: 2500-3500
// - Showcase: 4000-5000
// - 4K displays: Up to 8000 (with testing)
function getMaxParticlesForDevice() {
const ua = navigator.userAgent;
const width = window.innerWidth;
// Check for low-end mobile
if (/iPhone [1-7]|Android [1-5]/.test(ua)) {
return 500;
}
// Check for mobile
if (/iPhone|iPad|iPod|Android/i.test(ua)) {
return 1200;
}
// Check for tablet
if (/iPad|Android/i.test(ua) && width >= 768) {
return 2000;
}
// Check for laptop
if (width < 1440) {
return 2500;
}
// Check for desktop
if (width < 1920) {
return 3500;
}
// High-end desktop / 4K
return 5000;
}
const instance = initParticleJS('#canvas', {
text: 'ADAPTIVE',
colors: ['#695aa6'],
maxParticles: getMaxParticlesForDevice()
});

Subtle, non-distracting background animations:

const instance = initParticleJS('#canvas', {
text: 'BACKGROUND',
colors: ['#695aa6'],
maxParticles: 800, // Low count for efficiency
// Large particles for visibility
particleRadius: {
xs: { base: 3, rand: 1 }
},
// Minimal interaction
explosionRadius: {
xs: 60,
lg: 80
},
// Faster settling
particleFriction: 0.98
});
// Recommended range: 500-1000 particles
// Priority: Performance over visual density

Main focal point with good balance:

const instance = initParticleJS('#canvas', {
text: 'HERO',
colors: ['#695aa6'],
maxParticles: 2500, // Balanced count
// Standard particles
particleRadius: {
xs: { base: 2, rand: 1 }
},
// Good interaction
explosionRadius: {
xs: 80,
lg: 120
},
// Responsive to cursor
particleFriction: 0.95
});
// Recommended range: 2000-3000 particles
// Priority: Balance quality and performance

Maximum visual impact for portfolio pieces:

const instance = initParticleJS('#canvas', {
text: 'SHOWCASE',
colors: ['#695aa6'],
maxParticles: 4500, // High count for detail
// Fine particles
particleRadius: {
xs: { base: 2, rand: 0.5 }
},
// Dramatic interaction
explosionRadius: {
xs: 100,
lg: 150
},
// Smooth physics
particleFriction: 0.92
});
// Recommended range: 4000-5000 particles
// Priority: Visual quality (desktop only)
// Note: Consider lazy loading for performance

Clear, recognizable branding element:

const instance = initParticleJS('#canvas', {
text: 'LOGO',
colors: ['#695aa6'],
maxParticles: 1500, // Moderate count
// Larger font for impact
fontSize: 100,
// Medium particles
particleRadius: {
xs: { base: 2.5, rand: 1 }
},
// Moderate interaction
explosionRadius: {
xs: 70,
lg: 100
}
});
// Recommended range: 1000-2000 particles
// Priority: Clarity and brand recognition

Attention-grabbing interactive element:

const instance = initParticleJS('#canvas', {
text: 'CLICK ME',
colors: ['#FF6B6B', '#4ECDC4'],
maxParticles: 2000, // Good density
// Start animation on hover only
autoAnimate: false,
trackCursorOnlyInsideCanvas: true,
// Dramatic explosion
explosionRadius: {
xs: 100,
lg: 150
}
});
const canvas = document.getElementById('canvas');
canvas.addEventListener('mouseenter', () => {
instance.startAnimation();
});
canvas.addEventListener('mouseleave', () => {
instance.destroy();
});
// Recommended range: 1500-2500 particles
// Priority: Responsive interaction
// Ask these questions:
// 1. What's the primary device?
const targetDevice = 'mobile'; // 'mobile', 'tablet', 'desktop'
// 2. What's the text length?
const textLength = text.length; // Longer text needs more particles
// 3. What's the font size?
const fontSize = 80; // Larger fonts need more particles
// 4. What's the importance?
const importance = 'hero'; // 'background', 'hero', 'showcase'
// 5. What's the performance budget?
const targetFPS = 60; // 30, 45, 60
// Decision function
function calculateOptimalParticles(device, textLength, fontSize, importance, targetFPS) {
let base;
// Start with device baseline
if (device === 'mobile') base = 1000;
else if (device === 'tablet') base = 2000;
else base = 3000;
// Adjust for text length
if (textLength > 10) base *= 0.8; // Reduce for long text
if (textLength < 5) base *= 1.2; // Increase for short text
// Adjust for font size
base *= (fontSize / 80); // Scale proportionally
// Adjust for importance
if (importance === 'background') base *= 0.6;
if (importance === 'showcase') base *= 1.4;
// Adjust for FPS target
if (targetFPS === 60) base *= 0.8;
if (targetFPS === 30) base *= 1.3;
// Round to nearest 100
return Math.round(base / 100) * 100;
}
const optimalParticles = calculateOptimalParticles(
'desktop',
8, // "PARTICLE"
80,
'hero',
60
);
const instance = initParticleJS('#canvas', {
text: 'CALCULATED',
colors: ['#695aa6'],
maxParticles: optimalParticles
});
// Copy-paste configurations for common scenarios
// Mobile background
const mobileBackground = {
maxParticles: 600,
particleRadius: { xs: { base: 3, rand: 1 } },
explosionRadius: { xs: 50, lg: 70 }
};
// Mobile hero
const mobileHero = {
maxParticles: 1200,
particleRadius: { xs: { base: 2.5, rand: 1 } },
explosionRadius: { xs: 70, lg: 100 }
};
// Desktop background
const desktopBackground = {
maxParticles: 1500,
particleRadius: { xs: { base: 2.5, rand: 1 } },
explosionRadius: { xs: 70, lg: 100 }
};
// Desktop hero
const desktopHero = {
maxParticles: 2800,
particleRadius: { xs: { base: 2, rand: 1 } },
explosionRadius: { xs: 80, lg: 120 }
};
// Desktop showcase
const desktopShowcase = {
maxParticles: 4500,
particleRadius: { xs: { base: 2, rand: 0.5 } },
explosionRadius: { xs: 100, lg: 150 }
};
// 4K display showcase
const fourKShowcase = {
maxParticles: 6000,
particleRadius: { xs: { base: 1.5, rand: 0.5 } },
explosionRadius: { xs: 120, lg: 180 }
};
// Use appropriate config
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const isHero = document.querySelector('#canvas').classList.contains('hero');
let config;
if (isMobile && isHero) {
config = mobileHero;
} else if (isMobile) {
config = mobileBackground;
} else if (isHero) {
config = desktopHero;
} else {
config = desktopBackground;
}
const instance = initParticleJS('#canvas', {
text: 'OPTIMIZED',
colors: ['#695aa6'],
...config
});
const instance = initParticleJS('#canvas', {
text: 'ADAPTIVE',
colors: ['#695aa6'],
maxParticles: 3000,
// Automatically reduce particles if slow
renderTimeThreshold: 16, // Reduce if frame takes >16ms
// ParticleText.js will automatically reduce particle count
// if performance is poor, even if maxParticles is 3000
});
// How it works:
// 1. Starts with 3000 particles
// 2. Monitors frame render time
// 3. If frame time exceeds 16ms, reduces particle count
// 4. Result: Adaptive performance optimization
// Large explosion radius needs fewer particles
const sparse = initParticleJS('#canvas', {
text: 'SPARSE',
colors: ['#695aa6'],
maxParticles: 1200, // Fewer particles
explosionRadius: {
xs: 150, // Large explosion radius
lg: 200
}
});
// Fewer particles but strong interaction effect
// Small explosion radius needs more particles
const dense = initParticleJS('#canvas', {
text: 'DENSE',
colors: ['#695aa6'],
maxParticles: 4000, // More particles
explosionRadius: {
xs: 60, // Small explosion radius
lg: 80
}
});
// Many particles but subtle interaction effect
// Balance point
const balanced = initParticleJS('#canvas', {
text: 'BALANCED',
colors: ['#695aa6'],
maxParticles: 2500,
explosionRadius: {
xs: 80,
lg: 120
}
});
// Good particle density with good interaction
// Strategy: Fewer large particles
const fewLarge = initParticleJS('#canvas', {
text: 'LARGE',
colors: ['#695aa6'],
maxParticles: 1000, // Low count
particleRadius: {
xs: { base: 4, rand: 1 }, // Large particles
lg: { base: 4, rand: 1 }
}
});
// Fast performance, chunky aesthetic
// Strategy: Many small particles
const manySmall = initParticleJS('#canvas', {
text: 'SMALL',
colors: ['#695aa6'],
maxParticles: 5000, // High count
particleRadius: {
xs: { base: 1.5, rand: 0.5 }, // Small particles
lg: { base: 1.5, rand: 0.5 }
}
});
// Slower performance, fine-grained aesthetic
// Recommended: Balanced approach
const balanced = initParticleJS('#canvas', {
text: 'BALANCED',
colors: ['#695aa6'],
maxParticles: 2500,
particleRadius: {
xs: { base: 2, rand: 1 },
lg: { base: 2, rand: 1 }
}
});
// More colors = more variety needed
const multiColor = initParticleJS('#canvas', {
text: 'COLORFUL',
colors: ['#FF6B6B', '#4ECDC4', '#FFD93D', '#95E1D3', '#F38181'],
maxParticles: 3500, // Higher count for color distribution
// Optional: Weight colors
colorWeights: [2, 2, 1, 1, 1] // More red and cyan
});
// Need more particles to show all colors properly
// Fewer colors = fewer particles needed
const singleColor = initParticleJS('#canvas', {
text: 'SIMPLE',
colors: ['#695aa6'],
maxParticles: 2000, // Lower count sufficient
});
// Single color looks good with fewer particles
const instance = initParticleJS('#canvas', {
text: 'RESPONSIVE',
colors: ['#695aa6'],
// Different particle counts per breakpoint
maxParticles: 3000, // Desktop default
// Configure custom breakpoints
xs: {
maxParticles: 800, // Mobile
fontSize: 60
},
sm: {
maxParticles: 1200, // Tablet portrait
fontSize: 70
},
md: {
maxParticles: 2000, // Tablet landscape
fontSize: 80
},
lg: {
maxParticles: 3000, // Desktop
fontSize: 100
}
});
// Note: maxParticles in breakpoints is not officially supported
// Better approach: Reinitialize on resize
let currentBreakpoint = getCurrentBreakpoint();
window.addEventListener('resize', () => {
const newBreakpoint = getCurrentBreakpoint();
if (newBreakpoint !== currentBreakpoint) {
instance.destroy();
const particleCount = {
xs: 800,
sm: 1200,
md: 2000,
lg: 3000
}[newBreakpoint];
instance = initParticleJS('#canvas', {
text: 'RESPONSIVE',
colors: ['#695aa6'],
maxParticles: particleCount
});
currentBreakpoint = newBreakpoint;
}
});
function getCurrentBreakpoint() {
const width = window.innerWidth;
if (width < 640) return 'xs';
if (width < 768) return 'sm';
if (width < 1024) return 'md';
return 'lg';
}
// Don't waste particles on hidden animations
const instance = initParticleJS('#canvas', {
text: 'EFFICIENT',
colors: ['#695aa6'],
maxParticles: 4000, // Can afford more since not always running
autoAnimate: false, // Don't start immediately
trackCursorOnlyInsideCanvas: true
});
// Use Intersection Observer for lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
instance.startAnimation();
} else {
instance.destroy(); // Free up resources
}
});
}, {
threshold: 0.2
});
observer.observe(document.getElementById('canvas'));
// Result: Can use higher particle counts because animation
// only runs when visible
// Create a testing interface
const canvas = document.getElementById('canvas');
const slider = document.getElementById('particle-slider');
const display = document.getElementById('particle-count');
const fpsDisplay = document.getElementById('fps');
let instance;
let lastTime = performance.now();
let frameCount = 0;
function updateParticleCount(count) {
// Clean up previous instance
if (instance) {
instance.destroy();
}
// Create new instance with new count
instance = initParticleJS('#canvas', {
text: 'TEST',
colors: ['#695aa6'],
maxParticles: count
});
display.textContent = count;
}
// Slider control
slider.addEventListener('input', (e) => {
const count = parseInt(e.target.value);
updateParticleCount(count);
});
// FPS monitoring
function measureFPS() {
frameCount++;
const currentTime = performance.now();
const elapsed = currentTime - lastTime;
if (elapsed >= 1000) {
const fps = Math.round((frameCount * 1000) / elapsed);
fpsDisplay.textContent = `${fps} FPS`;
frameCount = 0;
lastTime = currentTime;
}
requestAnimationFrame(measureFPS);
}
measureFPS();
// HTML:
// <input type="range" id="particle-slider" min="100" max="8000" step="100" value="2000">
// <div>Particles: <span id="particle-count">2000</span></div>
// <div>FPS: <span id="fps">--</span></div>
// Test two configurations side-by-side
const canvas1 = initParticleJS('#canvas1', {
text: 'CONFIG A',
colors: ['#FF6B6B'],
maxParticles: 1500,
particleRadius: { xs: { base: 3, rand: 1 } }
});
const canvas2 = initParticleJS('#canvas2', {
text: 'CONFIG B',
colors: ['#4ECDC4'],
maxParticles: 3000,
particleRadius: { xs: { base: 2, rand: 1 } }
});
// Collect metrics
function collectMetrics(instance, name) {
const metrics = {
name: name,
particleCount: instance.particleList.length,
fps: 0,
frameTime: 0
};
let frameCount = 0;
let totalFrameTime = 0;
let lastTime = performance.now();
function measure() {
const startTime = performance.now();
// Measure frame time
frameCount++;
const currentTime = performance.now();
const frameTime = currentTime - startTime;
totalFrameTime += frameTime;
const elapsed = currentTime - lastTime;
if (elapsed >= 2000) { // 2 second sample
metrics.fps = Math.round((frameCount * 1000) / elapsed);
metrics.frameTime = (totalFrameTime / frameCount).toFixed(2);
console.log(`${metrics.name}:`, metrics);
frameCount = 0;
totalFrameTime = 0;
lastTime = currentTime;
}
requestAnimationFrame(measure);
}
measure();
}
collectMetrics(canvas1, 'Config A');
collectMetrics(canvas2, 'Config B');
const instance = initParticleJS('#canvas', {
text: 'MONITOR',
colors: ['#695aa6'],
maxParticles: 3000,
renderTimeThreshold: 16 // Auto-reduce if slow
});
// Monitor actual particle count
setInterval(() => {
const actualCount = instance.particleList.length;
const configuredMax = 3000;
console.log({
configured: configuredMax,
actual: actualCount,
reduced: actualCount < configuredMax,
percentage: ((actualCount / configuredMax) * 100).toFixed(1) + '%'
});
// If actual < configured, slow browser detection kicked in
if (actualCount < configuredMax) {
console.warn('Particle count reduced due to performance');
}
}, 1000);
// Collect performance data for analytics
function trackParticlePerformance(instance, config) {
const metrics = {
maxParticles: config.maxParticles,
actualParticles: instance.particleList.length,
deviceType: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) ? 'mobile' : 'desktop',
screenWidth: window.innerWidth,
userAgent: navigator.userAgent,
samples: []
};
let sampleCount = 0;
const maxSamples = 60; // Collect 60 samples (1 second at 60 FPS)
let startTime = performance.now();
function sample() {
const currentTime = performance.now();
const frameTime = currentTime - startTime;
metrics.samples.push(frameTime);
sampleCount++;
if (sampleCount >= maxSamples) {
// Calculate statistics
const avgFrameTime = metrics.samples.reduce((a, b) => a + b, 0) / metrics.samples.length;
const maxFrameTime = Math.max(...metrics.samples);
const minFrameTime = Math.min(...metrics.samples);
const avgFPS = 1000 / avgFrameTime;
const report = {
...metrics,
avgFrameTime: avgFrameTime.toFixed(2),
maxFrameTime: maxFrameTime.toFixed(2),
minFrameTime: minFrameTime.toFixed(2),
avgFPS: avgFPS.toFixed(1),
recommendation: getRecommendation(avgFPS, metrics.maxParticles)
};
// Send to analytics
console.log('Performance Report:', report);
// analytics.track('particle_performance', report);
return;
}
startTime = currentTime;
requestAnimationFrame(sample);
}
requestAnimationFrame(sample);
}
function getRecommendation(fps, maxParticles) {
if (fps >= 55) {
return `Excellent performance. Could increase to ${Math.round(maxParticles * 1.3)} particles.`;
} else if (fps >= 45) {
return 'Good performance. Current settings optimal.';
} else if (fps >= 30) {
return `Fair performance. Consider reducing to ${Math.round(maxParticles * 0.7)} particles.`;
} else {
return `Poor performance. Reduce to ${Math.round(maxParticles * 0.5)} particles.`;
}
}
// Use it
const instance = initParticleJS('#canvas', {
text: 'ANALYTICS',
colors: ['#695aa6'],
maxParticles: 3000
});
trackParticlePerformance(instance, { maxParticles: 3000 });
// ✅ Good: Start low, increase if needed
let instance = initParticleJS('#canvas', {
text: 'START LOW',
colors: ['#695aa6'],
maxParticles: 1500 // Conservative starting point
});
// Test performance, then increase if smooth
setTimeout(() => {
const fps = measureCurrentFPS();
if (fps > 55) {
console.log('Performance good, can increase particles');
instance.destroy();
instance = initParticleJS('#canvas', {
text: 'INCREASED',
colors: ['#695aa6'],
maxParticles: 2500
});
}
}, 2000);
// ❌ Bad: Start with maximum, users experience lag immediately
const instance = initParticleJS('#canvas', {
text: 'TOO HIGH',
colors: ['#695aa6'],
maxParticles: 8000 // Likely to cause lag
});
// ✅ Good: Test on actual target devices
// Test checklist:
// - iPhone SE (low-end mobile)
// - iPhone 12+ (modern mobile)
// - iPad (tablet)
// - MacBook Pro (laptop)
// - Desktop (high-end)
// - 4K display (ultra high-end)
const testConfigs = [
{ device: 'iPhone SE', maxParticles: 800 },
{ device: 'iPhone 12', maxParticles: 1500 },
{ device: 'iPad', maxParticles: 2000 },
{ device: 'Laptop', maxParticles: 2800 },
{ device: 'Desktop', maxParticles: 4000 }
];
// Record actual FPS on each device
// Adjust maxParticles based on results
// ❌ Bad: Only test on your development machine
const instance = initParticleJS('#canvas', {
text: 'UNTESTED',
colors: ['#695aa6'],
maxParticles: 5000 // Works on M1 MacBook Pro, might fail on older devices
});
// ✅ Good: Let users adjust if needed
const qualitySettings = {
low: 1000,
medium: 2500,
high: 5000
};
// Load from localStorage or default to 'medium'
let quality = localStorage.getItem('particleQuality') || 'medium';
let instance = initParticleJS('#canvas', {
text: 'QUALITY',
colors: ['#695aa6'],
maxParticles: qualitySettings[quality]
});
// Provide UI control
document.getElementById('quality-select').addEventListener('change', (e) => {
quality = e.target.value;
localStorage.setItem('particleQuality', quality);
instance.destroy();
instance = initParticleJS('#canvas', {
text: 'QUALITY',
colors: ['#695aa6'],
maxParticles: qualitySettings[quality]
});
});
// HTML:
// <select id="quality-select">
// <option value="low">Low (Better Performance)</option>
// <option value="medium" selected>Medium (Balanced)</option>
// <option value="high">High (Better Quality)</option>
// </select>
// ❌ Bad: Force one setting for all users
const instance = initParticleJS('#canvas', {
text: 'FIXED',
colors: ['#695aa6'],
maxParticles: 5000 // No user control
});
// ✅ Good: Combine with automatic adaptation
const instance = initParticleJS('#canvas', {
text: 'ADAPTIVE',
colors: ['#695aa6'],
maxParticles: 3000, // Starting point
// Enable automatic reduction
renderTimeThreshold: 16, // Reduce if frame > 16ms (60 FPS)
});
// Library will automatically reduce particle count if needed
// You set the maximum, library ensures good performance
// ❌ Bad: High fixed count without fallback
const instance = initParticleJS('#canvas', {
text: 'RISKY',
colors: ['#695aa6'],
maxParticles: 5000,
renderTimeThreshold: 9999 // Effectively disabled
});
// Will lag on slower devices with no adaptation
// ✅ Good: Clear documentation
const instance = initParticleJS('#canvas', {
text: 'HERO',
colors: ['#695aa6'],
// Set to 2500 after testing on:
// - iPhone 12: 45-50 FPS
// - MacBook Pro M1: 60 FPS
// - Desktop (RTX 3080): 60 FPS
// Trade-off: Slightly lower quality on mobile, but smooth animation
maxParticles: 2500
});
// ❌ Bad: Magic numbers without context
const instance = initParticleJS('#canvas', {
text: 'MYSTERY',
colors: ['#695aa6'],
maxParticles: 2137 // Why this specific number?
});
const instance = initParticleJS('#canvas', {
text: 'TEST',
colors: ['#695aa6'],
maxParticles: 5000
});
console.log('Expected:', 5000);
console.log('Actual:', instance.particleList.length);
// If actual < expected, possible reasons:
// 1. Slow browser detection reduced count
// Check: renderTimeThreshold setting
if (instance.particleList.length < 5000) {
console.log('Slow browser detection may have reduced particles');
// Solution: Increase renderTimeThreshold or disable
instance.destroy();
instance = initParticleJS('#canvas', {
text: 'TEST',
colors: ['#695aa6'],
maxParticles: 5000,
renderTimeThreshold: 9999 // Effectively disable
});
}
// 2. Text is simple/small (not enough pixels to generate particles)
// Check: Font size and text length
console.log('Text length:', instance.text.length);
// Solution: Increase fontSize or use longer text
// 3. Particle sampling skipped some pixels
// This is normal - not every pixel becomes a particle
// Solution: Increase font size or reduce particle radius
// If setting low maxParticles doesn't help:
// 1. Check other factors
const instance = initParticleJS('#canvas', {
text: 'SLOW',
colors: ['#695aa6'],
maxParticles: 1000, // Already low
// Other performance factors:
explosionRadius: { xs: 50, lg: 70 }, // Reduce explosion radius
particleRadius: { xs: { base: 3, rand: 1 } }, // Larger particles (less rendering)
particleFriction: 0.98, // Faster settling
trackCursorOnlyInsideCanvas: true // Reduce calculations
});
// 2. Use manual animation control
instance.destroy();
const betterInstance = initParticleJS('#canvas', {
text: 'OPTIMIZED',
colors: ['#695aa6'],
maxParticles: 1000,
autoAnimate: false
});
// Only animate when visible
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
betterInstance.startAnimation();
} else {
betterInstance.destroy();
}
});
});
observer.observe(document.getElementById('canvas'));
// 3. Throttle frame rate
let lastFrame = 0;
const fps30 = 1000 / 30;
function throttledLoop(currentTime) {
if (currentTime - lastFrame >= fps30) {
betterInstance.forceRequestAnimationFrame();
lastFrame = currentTime;
}
requestAnimationFrame(throttledLoop);
}
// If increasing maxParticles causes lag:
// Solution 1: Compensate with larger particles
const instance = initParticleJS('#canvas', {
text: 'QUALITY',
colors: ['#695aa6'],
maxParticles: 1500, // Keep low for performance
particleRadius: {
xs: { base: 3.5, rand: 1 }, // Larger particles fill gaps
lg: { base: 3.5, rand: 1 }
}
});
// Solution 2: Increase font size
const instance2 = initParticleJS('#canvas', {
text: 'BIG',
colors: ['#695aa6'],
maxParticles: 1500,
fontSize: 120 // Larger text = better coverage with fewer particles
});
// Solution 3: Use shorter text
const instance3 = initParticleJS('#canvas', {
text: 'HI', // 2 chars instead of 8
colors: ['#695aa6'],
maxParticles: 1500 // Same budget, better density per letter
});
// Solution 4: Target only desktop users
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
// Show static image or simpler effect
document.getElementById('canvas').style.display = 'none';
document.getElementById('fallback-image').style.display = 'block';
} else {
// Full quality on desktop
const instance = initParticleJS('#canvas', {
text: 'DESKTOP',
colors: ['#695aa6'],
maxParticles: 5000
});
}
// If particleList.length varies over time:
const instance = initParticleJS('#canvas', {
text: 'DYNAMIC',
colors: ['#695aa6'],
maxParticles: 3000,
renderTimeThreshold: 16 // Slow browser detection enabled
});
// Monitor changes
let lastCount = instance.particleList.length;
setInterval(() => {
const currentCount = instance.particleList.length;
if (currentCount !== lastCount) {
console.log(`Particle count changed: ${lastCount}${currentCount}`);
console.log('Reason: Slow browser detection adjusted count');
lastCount = currentCount;
}
}, 1000);
// This is expected behavior when renderTimeThreshold is set
// To prevent changes, set very high threshold:
instance.destroy();
const stable = initParticleJS('#canvas', {
text: 'STABLE',
colors: ['#695aa6'],
maxParticles: 3000,
renderTimeThreshold: 9999 // Won't auto-adjust
});
// Note: Particle count can only decrease, never increase
// Once reduced, it stays reduced