| date | 2025-12-16 07:13:20 +0530 |
|---|---|
| description | Build cursor tracking systems with position monitoring, visual effects, analytics integration, and performance optimization for interactive web applications. |
| premium | true |
Interview Importance: 🟡 Important — Cursor tracking appears in 20% of frontend machine coding interviews, testing event handling, performance optimization, debouncing/throttling, and real-time UI updates. Common in companies building interactive experiences or analytics platforms.
Cursor Tracking is the process of monitoring and recording mouse cursor movements, positions, and interactions on a web page. It captures the x and y coordinates of the cursor in real-time and can be used to create interactive visual effects, collect user behavior analytics, implement custom cursors, or build collaborative features.
Visual Representation:
Browser Window (viewport)
+-----------------------------------------+
| (0,0) |
| \ |
| |
| 🖱️ (x: 450, y: 320) |
| Cursor Position |
| |
| |
| (1920,1080)|
+-----------------------------------------+
Coordinate System:
- Origin (0,0) = Top-left corner
- X increases -> right
- Y increases -> down
- clientX/Y = relative to viewport
- pageX/Y = relative to document (includes scroll)
Real-World Analogy:
Think of cursor tracking like GPS tracking on your phone. Just as GPS continuously monitors your location to provide navigation or track your running route, cursor tracking monitors the mouse position to enable features like heatmaps showing where users click most, spotlight effects following the cursor, or custom cursors that change based on what you're hovering over.
| Use Case | Problem | Solution with Cursor Tracking |
|---|---|---|
| Spotlight Effects | Need to highlight area under cursor | Track position and render circular highlight div that follows cursor |
| Custom Cursors | Default cursor doesn't match brand/UX | Hide default cursor, render custom element at cursor coordinates |
| User Analytics | Don't know where users focus attention | Record cursor movements, generate heatmaps, identify UX issues |
| Interactive Parallax | Static backgrounds feel boring | Track cursor position to shift background layers, creating depth |
| Tooltips & Popovers | Need contextual information near cursor | Position tooltip elements relative to current cursor coordinates |
| Drawing/Canvas Apps | Need to capture user drawing input | Track cursor position and state to draw lines on canvas |
| Collaborative Tools | Team members can't see each other's cursors | Broadcast cursor positions via WebSocket, render multiple cursors |
| Accessibility Testing | Need to understand user navigation patterns | Record cursor trails to analyze how users explore the interface |
Performance Benefits:
- Real-time feedback: Updates at 60fps for smooth user experience
- Optimizable: Can use throttling/debouncing to reduce event frequency (from 100+ events/sec to 10-20)
- Composable: Works with other events (click, scroll, drag) for rich interactions
- Analytics-ready: Captures detailed user behavior data for UX improvements
// Basic cursor tracker that displays x, y coordinates
const createCursorTracker = () => {
// Create display element
const display = document.createElement('div');
display.id = 'cursor-position';
display.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 5px;
font-family: monospace;
font-size: 14px;
z-index: 9999;
pointer-events: none;
`;
document.body.appendChild(display);
// Track cursor movement
const handleMouseMove = (event) => {
const { clientX, clientY, pageX, pageY } = event;
// Update display with current position
display.innerHTML = `
<div>Client: (${clientX}, ${clientY})</div>
<div>Page: (${pageX}, ${pageY})</div>
`;
};
// Attach event listener
document.addEventListener('mousemove', handleMouseMove);
// Return cleanup function
return () => {
document.removeEventListener('mousemove', handleMouseMove);
display.remove();
};
};
// Usage
const cleanup = createCursorTracker();
// Call cleanup() to remove trackerScenario: User moves cursor from (100, 100) to (200, 150)
Step 1: Initialize tracker
---------------------------------------------------------
display element created and appended to body
Event listener attached to document
State: Waiting for mouse movement
Step 2: User moves cursor to (100, 100)
---------------------------------------------------------
Event: mousemove fired
event.clientX = 100
event.clientY = 100
event.pageX = 100 (no scroll)
event.pageY = 100 (no scroll)
Action: display.innerHTML updated
Display shows: "Client: (100, 100), Page: (100, 100)"
Step 3: User moves cursor to (150, 125)
---------------------------------------------------------
Event: mousemove fired again
event.clientX = 150
event.clientY = 125
event.pageX = 150
event.pageY = 125
Action: display.innerHTML updated
Display shows: "Client: (150, 125), Page: (150, 125)"
Step 4: User moves cursor to (200, 150)
---------------------------------------------------------
Event: mousemove fired again
event.clientX = 200
event.clientY = 150
event.pageX = 200
event.pageY = 150
Action: display.innerHTML updated
Display shows: "Client: (200, 150), Page: (200, 150)"
Step 5: User scrolls page down by 500px, cursor at same viewport position
---------------------------------------------------------
Event: mousemove fired (cursor technically moved in document)
event.clientX = 200 (viewport position unchanged)
event.clientY = 150 (viewport position unchanged)
event.pageX = 200 (document X position same - horizontal scroll is 0)
event.pageY = 650 (150 + 500 scroll offset)
Action: display.innerHTML updated
Display shows: "Client: (200, 150), Page: (200, 650)"
Result: Real-time position display tracking both viewport and document coordinates
Key Insight: The mousemove event fires approximately 60-100 times per second during movement, providing smooth real-time tracking.
// ✅ GOOD: Track across entire page
document.addEventListener('mousemove', handler);
// ❌ LIMITED: Only tracks within specific element
element.addEventListener('mousemove', handler);What breaks without it: If you attach to a specific element, you lose tracking when cursor moves outside that element. Document-level tracking ensures continuous monitoring.
event.clientX // Position relative to viewport (visible window area)
event.clientY // Best for: positioning fixed elements, tooltips
event.pageX // Position relative to entire document (includes scroll)
event.pageY // Best for: absolute positioned elements, analytics
event.screenX // Position relative to user's physical screen
event.screenY // Best for: multi-monitor setups, rarely usedWhat breaks without understanding: Using clientX for absolute positioned elements causes misalignment when page is scrolled. Using pageX for fixed elements causes them to jump around.
display.style.pointerEvents = 'none';What breaks without it: The tracking display itself blocks mouse events, creating a "cursor trap" where the cursor appears to flicker or the display blocks interactions with elements beneath it.
// Always return cleanup function
return () => {
document.removeEventListener('mousemove', handleMouseMove);
display.remove();
};What breaks without it: Event listeners persist after component unmount, causing memory leaks. Multiple trackers accumulate, degrading performance. Zombie listeners fire for non-existent components.
- Touch devices: No cursor, events don't fire (need touch events)
- Cursor leaves window: Last known position persists
- High frequency: 60-100 events/sec can overwhelm slow handlers
- Passive listeners: Add
{ passive: true }for better scroll performance
class CursorTracker {
constructor(options = {}) {
// Configuration with defaults
this.config = {
throttleMs: options.throttleMs || 16, // ~60fps
enableAnalytics: options.enableAnalytics || false,
enableVisual: options.enableVisual || true,
recordHistory: options.recordHistory || false,
maxHistorySize: options.maxHistorySize || 1000,
onMove: options.onMove || null, // Callback for custom handling
...options
};
// State
this.position = { x: 0, y: 0, pageX: 0, pageY: 0, timestamp: null };
this.history = [];
this.isActive = false;
this.visualElement = null;
this.throttleTimer = null;
this.lastEmitTime = 0;
// Bind methods
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
}
// Initialize tracker
init() {
if (this.isActive) {
console.warn('CursorTracker: Already initialized');
return;
}
// Create visual indicator if enabled
if (this.config.enableVisual) {
this.createVisualElement();
}
// Attach event listeners
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseleave', this.handleMouseLeave);
document.addEventListener('mouseenter', this.handleMouseEnter);
this.isActive = true;
console.log('CursorTracker: Initialized');
}
// Create visual indicator element
createVisualElement() {
this.visualElement = document.createElement('div');
this.visualElement.id = 'cursor-tracker-visual';
this.visualElement.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.85);
color: #00ff00;
padding: 12px 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
z-index: 999999;
pointer-events: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
`;
document.body.appendChild(this.visualElement);
}
// Update visual display
updateVisual() {
if (!this.visualElement) return;
const { x, y, pageX, pageY, timestamp } = this.position;
const historySize = this.history.length;
this.visualElement.innerHTML = `
<div style="margin-bottom: 4px; color: #00ffff;">📍 CURSOR TRACKER</div>
<div>Viewport: (${x}, ${y})</div>
<div>Document: (${pageX}, ${pageY})</div>
<div style="margin-top: 4px; opacity: 0.7;">History: ${historySize} points</div>
`;
}
// Throttled mouse move handler
handleMouseMove(event) {
const now = Date.now();
// Throttle: only emit at specified interval
if (now - this.lastEmitTime < this.config.throttleMs) {
return;
}
this.lastEmitTime = now;
// Update position
this.position = {
x: event.clientX,
y: event.clientY,
pageX: event.pageX,
pageY: event.pageY,
timestamp: now
};
// Record to history if enabled
if (this.config.recordHistory) {
this.addToHistory(this.position);
}
// Update visual display
if (this.config.enableVisual) {
this.updateVisual();
}
// Send to analytics if enabled
if (this.config.enableAnalytics) {
this.sendAnalytics(this.position);
}
// Call custom callback if provided
if (typeof this.config.onMove === 'function') {
this.config.onMove(this.position);
}
}
// Add position to history with size limit
addToHistory(position) {
this.history.push({ ...position });
// Trim history if exceeds max size
if (this.history.length > this.config.maxHistorySize) {
this.history.shift();
}
}
// Handle cursor leaving window
handleMouseLeave() {
console.log('CursorTracker: Cursor left window');
}
// Handle cursor entering window
handleMouseEnter() {
console.log('CursorTracker: Cursor entered window');
}
// Send analytics data (mock implementation)
sendAnalytics(position) {
// In production, this would send to your analytics service
// Examples: Google Analytics, Mixpanel, custom endpoint
console.log('Analytics:', {
event: 'cursor_move',
x: position.x,
y: position.y,
timestamp: position.timestamp
});
}
// Get current position
getPosition() {
return { ...this.position };
}
// Get movement history
getHistory() {
return [...this.history];
}
// Calculate cursor speed (pixels per second)
getSpeed() {
if (this.history.length < 2) return 0;
const recent = this.history.slice(-10);
let totalDistance = 0;
for (let i = 1; i < recent.length; i++) {
const dx = recent[i].x - recent[i-1].x;
const dy = recent[i].y - recent[i-1].y;
totalDistance += Math.sqrt(dx * dx + dy * dy);
}
const timeSpan = recent[recent.length - 1].timestamp - recent[0].timestamp;
return (totalDistance / timeSpan) * 1000; // pixels per second
}
// Get heatmap data (simplified)
getHeatmapData(gridSize = 50) {
const heatmap = {};
this.history.forEach(pos => {
const gridX = Math.floor(pos.x / gridSize);
const gridY = Math.floor(pos.y / gridSize);
const key = `${gridX},${gridY}`;
heatmap[key] = (heatmap[key] || 0) + 1;
});
return heatmap;
}
// Clear history
clearHistory() {
this.history = [];
console.log('CursorTracker: History cleared');
}
// Destroy tracker and cleanup
destroy() {
if (!this.isActive) return;
// Remove event listeners
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseleave', this.handleMouseLeave);
document.removeEventListener('mouseenter', this.handleMouseEnter);
// Remove visual element
if (this.visualElement) {
this.visualElement.remove();
this.visualElement = null;
}
// Clear timers
if (this.throttleTimer) {
clearTimeout(this.throttleTimer);
}
// Reset state
this.history = [];
this.isActive = false;
console.log('CursorTracker: Destroyed and cleaned up');
}
}
// -------------------------
// 🧪 Usage Examples
// -------------------------
// Basic usage
const tracker = new CursorTracker({
throttleMs: 16, // 60fps
enableAnalytics: false,
enableVisual: true,
recordHistory: true
});
tracker.init();
// With custom callback
const trackerWithCallback = new CursorTracker({
throttleMs: 50, // 20fps for better performance
enableVisual: false,
onMove: (position) => {
console.log(`Cursor at (${position.x}, ${position.y})`);
}
});
trackerWithCallback.init();
// Get analytics data
setTimeout(() => {
const speed = tracker.getSpeed();
const heatmap = tracker.getHeatmapData();
console.log('Cursor speed:', speed, 'px/s');
console.log('Heatmap data:', heatmap);
}, 5000);
// Cleanup when done
// tracker.destroy();class SpotlightCursor {
constructor(spotlightRadius = 150) {
this.radius = spotlightRadius;
this.spotlight = null;
this.overlay = null;
this.handleMouseMove = this.handleMouseMove.bind(this);
}
init() {
// Create dark overlay
this.overlay = document.createElement('div');
this.overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
pointer-events: none;
z-index: 999998;
`;
document.body.appendChild(this.overlay);
// Create spotlight circle
this.spotlight = document.createElement('div');
this.spotlight.style.cssText = `
position: fixed;
width: ${this.radius * 2}px;
height: ${this.radius * 2}px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 70%);
mix-blend-mode: destination-out;
pointer-events: none;
z-index: 999999;
transform: translate(-50%, -50%);
`;
document.body.appendChild(this.spotlight);
document.addEventListener('mousemove', this.handleMouseMove);
}
handleMouseMove(event) {
const { clientX, clientY } = event;
this.spotlight.style.left = `${clientX}px`;
this.spotlight.style.top = `${clientY}px`;
}
destroy() {
document.removeEventListener('mousemove', this.handleMouseMove);
if (this.overlay) this.overlay.remove();
if (this.spotlight) this.spotlight.remove();
}
}
// Usage
const spotlight = new SpotlightCursor(200);
spotlight.init();
// spotlight.destroy(); // to removeimport { useState, useEffect, useRef, useCallback } from 'react';
// Custom hook for cursor tracking
const useCursorTracking = (options = {}) => {
const {
throttleMs = 16,
enableHistory = false,
maxHistorySize = 100
} = options;
const [position, setPosition] = useState({ x: 0, y: 0 });
const historyRef = useRef([]);
const lastUpdateRef = useRef(0);
const handleMouseMove = useCallback((event) => {
const now = Date.now();
// Throttle updates
if (now - lastUpdateRef.current < throttleMs) {
return;
}
lastUpdateRef.current = now;
const newPosition = {
x: event.clientX,
y: event.clientY,
pageX: event.pageX,
pageY: event.pageY,
timestamp: now
};
setPosition(newPosition);
// Add to history if enabled
if (enableHistory) {
historyRef.current.push(newPosition);
if (historyRef.current.length > maxHistorySize) {
historyRef.current.shift();
}
}
}, [throttleMs, enableHistory, maxHistorySize]);
useEffect(() => {
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, [handleMouseMove]);
return {
position,
history: historyRef.current
};
};
// Component using the hook
const CursorDisplay = () => {
const { position } = useCursorTracking({ throttleMs: 16 });
return (
<div style={{
position: 'fixed',
top: 10,
left: 10,
background: 'rgba(0,0,0,0.8)',
color: 'white',
padding: '10px 15px',
borderRadius: 5,
fontFamily: 'monospace',
pointerEvents: 'none',
zIndex: 9999
}}>
<div>X: {position.x}</div>
<div>Y: {position.y}</div>
</div>
);
};
// Custom cursor component
const CustomCursor = () => {
const { position } = useCursorTracking({ throttleMs: 8 });
const [isHovering, setIsHovering] = useState(false);
useEffect(() => {
// Hide default cursor
document.body.style.cursor = 'none';
return () => {
document.body.style.cursor = 'auto';
};
}, []);
// Check if hovering over interactive elements
useEffect(() => {
const checkHover = () => {
const elements = document.elementsFromPoint(position.x, position.y);
const interactive = elements.some(el =>
el.tagName === 'A' ||
el.tagName === 'BUTTON' ||
el.onclick !== null
);
setIsHovering(interactive);
};
checkHover();
}, [position]);
return (
<div style={{
position: 'fixed',
left: position.x,
top: position.y,
width: isHovering ? 40 : 20,
height: isHovering ? 40 : 20,
borderRadius: '50%',
border: '2px solid #00ffff',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
zIndex: 999999,
transition: 'width 0.2s, height 0.2s',
mixBlendMode: 'difference'
}} />
);
};
export { useCursorTracking, CursorDisplay, CustomCursor };const CursorTrail = () => {
const [trail, setTrail] = useState([]);
const maxTrailLength = 20;
useEffect(() => {
const handleMouseMove = (e) => {
setTrail(prev => {
const newTrail = [
...prev,
{ x: e.clientX, y: e.clientY, id: Date.now() }
];
// Keep only last N points
return newTrail.slice(-maxTrailLength);
});
};
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
}, []);
return (
<>
{trail.map((point, index) => (
<div
key={point.id}
style={{
position: 'fixed',
left: point.x,
top: point.y,
width: 10,
height: 10,
borderRadius: '50%',
background: `rgba(0, 255, 255, ${(index + 1) / maxTrailLength})`,
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
zIndex: 999999 - index
}}
/>
))}
</>
);
};Answer: Several optimization strategies:
- Throttling: Limit event processing frequency
let lastTime = 0;
const throttleMs = 16; // ~60fps
document.addEventListener('mousemove', (e) => {
const now = Date.now();
if (now - lastTime < throttleMs) return;
lastTime = now;
// Process event
});- Passive event listeners: Improve scroll performance
document.addEventListener('mousemove', handler, { passive: true });- RequestAnimationFrame: Sync with browser paint cycles
let rafId = null;
let lastPosition = null;
document.addEventListener('mousemove', (e) => {
lastPosition = { x: e.clientX, y: e.clientY };
if (rafId === null) {
rafId = requestAnimationFrame(() => {
updateVisual(lastPosition);
rafId = null;
});
}
});- Avoid DOM manipulation in event handler: Batch updates
- Use transform instead of top/left: Triggers GPU acceleration
Answer:
- clientX/clientY: Relative to viewport (visible area). Best for fixed positioned elements like tooltips.
- pageX/pageY: Relative to entire document (includes scroll offset). Best for absolute positioned elements.
- screenX/screenY: Relative to user's physical screen. Rarely used, helpful for multi-monitor setups.
Example:
// If viewport is scrolled 100px down
event.clientX = 200 // Position in visible window
event.pageX = 200 // Same X (no horizontal scroll)
event.clientY = 150 // Position in visible window
event.pageY = 250 // 150 + 100 scroll offsetAnswer: Use unified event handling for both mouse and touch:
const getPosition = (event) => {
// Touch event
if (event.touches && event.touches.length > 0) {
return {
x: event.touches[0].clientX,
y: event.touches[0].clientY
};
}
// Mouse event
return {
x: event.clientX,
y: event.clientY
};
};
const handleMove = (event) => {
const pos = getPosition(event);
updatePosition(pos);
};
// Listen to both event types
document.addEventListener('mousemove', handleMove);
document.addEventListener('touchmove', handleMove, { passive: false });Answer:
class CursorTracker {
constructor() {
// Bind methods to preserve context
this.handleMove = this.handleMove.bind(this);
}
init() {
// Store reference for cleanup
document.addEventListener('mousemove', this.handleMove);
}
destroy() {
// Remove event listener using same reference
document.removeEventListener('mousemove', this.handleMove);
// Clear any timers
if (this.throttleTimer) {
clearTimeout(this.throttleTimer);
}
// Remove DOM elements
if (this.visualElement) {
this.visualElement.remove();
}
// Clear history array
this.history = [];
}
}Key points:
- Always remove event listeners in cleanup
- Clear timers and intervals
- Remove added DOM elements
- Clear large data structures (history arrays)
- In React, use useEffect cleanup function
Answer:
class HeatmapGenerator {
constructor(gridSize = 50) {
this.gridSize = gridSize;
this.heatmapData = new Map();
}
recordPosition(x, y) {
// Convert to grid coordinates
const gridX = Math.floor(x / this.gridSize);
const gridY = Math.floor(y / this.gridSize);
const key = `${gridX},${gridY}`;
// Increment count
const count = this.heatmapData.get(key) || 0;
this.heatmapData.set(key, count + 1);
}
generateHeatmap() {
const maxCount = Math.max(...this.heatmapData.values());
const heatmap = [];
this.heatmapData.forEach((count, key) => {
const [x, y] = key.split(',').map(Number);
const intensity = count / maxCount; // Normalize 0-1
heatmap.push({
x: x * this.gridSize,
y: y * this.gridSize,
intensity,
count
});
});
return heatmap;
}
renderHeatmap(container) {
const heatmap = this.generateHeatmap();
heatmap.forEach(cell => {
const div = document.createElement('div');
div.style.cssText = `
position: absolute;
left: ${cell.x}px;
top: ${cell.y}px;
width: ${this.gridSize}px;
height: ${this.gridSize}px;
background: rgba(255, 0, 0, ${cell.intensity * 0.6});
pointer-events: none;
`;
container.appendChild(div);
});
}
}Answer:
class CursorSpeedTracker {
constructor() {
this.positions = [];
this.maxHistorySize = 10;
}
recordPosition(x, y, timestamp) {
this.positions.push({ x, y, timestamp });
if (this.positions.length > this.maxHistorySize) {
this.positions.shift();
}
}
getSpeed() {
if (this.positions.length < 2) return 0;
let totalDistance = 0;
const positions = this.positions;
for (let i = 1; i < positions.length; i++) {
const dx = positions[i].x - positions[i-1].x;
const dy = positions[i].y - positions[i-1].y;
const distance = Math.sqrt(dx * dx + dy * dy);
totalDistance += distance;
}
const timeSpan = positions[positions.length - 1].timestamp - positions[0].timestamp;
return (totalDistance / timeSpan) * 1000; // pixels per second
}
getAcceleration() {
if (this.positions.length < 3) return 0;
const positions = this.positions;
const recentSpeeds = [];
for (let i = 1; i < positions.length; i++) {
const dx = positions[i].x - positions[i-1].x;
const dy = positions[i].y - positions[i-1].y;
const distance = Math.sqrt(dx * dx + dy * dy);
const time = positions[i].timestamp - positions[i-1].timestamp;
const speed = (distance / time) * 1000;
recentSpeeds.push(speed);
}
// Calculate acceleration (change in speed)
let totalAccel = 0;
for (let i = 1; i < recentSpeeds.length; i++) {
totalAccel += recentSpeeds[i] - recentSpeeds[i-1];
}
return totalAccel / (recentSpeeds.length - 1);
}
}❌ BAD: Processing every mousemove event
// Fires 60-100 times per second!
document.addEventListener('mousemove', (e) => {
// Heavy DOM manipulation
updateComplexUI(e.clientX, e.clientY);
// Network request on every move (terrible!)
fetch('/api/track', {
method: 'POST',
body: JSON.stringify({ x: e.clientX, y: e.clientY })
});
});Problem: Causes severe performance degradation. Can freeze UI, drop frames, drain battery, overload server.
✅ GOOD: Throttled event processing
let lastTime = 0;
const throttleMs = 50; // 20fps, good balance
document.addEventListener('mousemove', (e) => {
const now = Date.now();
if (now - lastTime < throttleMs) return;
lastTime = now;
updateComplexUI(e.clientX, e.clientY);
});
// Batch API calls every 5 seconds
const positionBuffer = [];
setInterval(() => {
if (positionBuffer.length > 0) {
fetch('/api/track', {
method: 'POST',
body: JSON.stringify({ positions: positionBuffer })
});
positionBuffer.length = 0;
}
}, 5000);❌ BAD: Memory leak in React component
const CursorTracker = () => {
const [pos, setPos] = useState({ x: 0, y: 0 });
// ❌ Event listener never removed!
useEffect(() => {
document.addEventListener('mousemove', (e) => {
setPos({ x: e.clientX, y: e.clientY });
});
}, []); // No cleanup function
return <div>X: {pos.x}, Y: {pos.y}</div>;
};Problem: Every time component remounts, a new listener is added. After 100 mounts, you have 100 listeners all updating state, causing severe performance issues and memory leaks.
✅ GOOD: Proper cleanup
const CursorTracker = () => {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPos({ x: e.clientX, y: e.clientY });
};
document.addEventListener('mousemove', handleMove);
// ✅ Cleanup function removes listener
return () => {
document.removeEventListener('mousemove', handleMove);
};
}, []);
return <div>X: {pos.x}, Y: {pos.y}</div>;
};❌ BAD: Using clientX for absolute positioned elements
const tooltip = document.createElement('div');
tooltip.style.position = 'absolute'; // Wrong! Should be 'fixed' for clientX
document.addEventListener('mousemove', (e) => {
// ❌ Breaks when page is scrolled
tooltip.style.left = `${e.clientX}px`;
tooltip.style.top = `${e.clientY}px`;
});Problem: When user scrolls page down by 500px, tooltip appears 500px above cursor because clientX/Y is viewport-relative but absolute positioning is document-relative.
✅ GOOD: Match coordinate system to positioning
const tooltip = document.createElement('div');
tooltip.style.position = 'fixed'; // ✅ Fixed with clientX
document.addEventListener('mousemove', (e) => {
tooltip.style.left = `${e.clientX}px`;
tooltip.style.top = `${e.clientY}px`;
});
// OR use absolute with pageX
const absoluteElement = document.createElement('div');
absoluteElement.style.position = 'absolute'; // ✅ Absolute with pageX
document.addEventListener('mousemove', (e) => {
absoluteElement.style.left = `${e.pageX}px`;
absoluteElement.style.top = `${e.pageY}px`;
});❌ BAD: Tracker blocks mouse events
const tracker = document.createElement('div');
tracker.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
width: 200px;
height: 100px;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
`;
// ❌ No pointer-events: none
document.body.appendChild(tracker);Problem: Tracker blocks clicks and hovers on elements beneath it. Buttons become unclickable, links don't work in that area.
✅ GOOD: Make tracking elements non-interactive
const tracker = document.createElement('div');
tracker.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
width: 200px;
height: 100px;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
pointer-events: none; /* ✅ Allows events to pass through */
`;
document.body.appendChild(tracker);| Operation | Time Complexity | Space Complexity | Explanation |
|---|---|---|---|
| Track single position | O(1) | O(1) | Simple variable update |
| Update visual element | O(1) | O(1) | Direct DOM property modification |
| Record to history (no limit) | O(1) amortized | O(n) where n = history size | Array push is O(1), grows linearly |
| Record to history (with limit) | O(1) | O(k) where k = max size | Circular buffer or shift when full |
| Calculate speed from history | O(h) where h = recent points | O(1) | Loop through recent history |
| Generate heatmap | O(n) where n = positions | O(g) where g = grid cells | Grid-based bucketing |
| Throttled event handling | O(1) per event | O(1) | Simple timestamp comparison |
| Render trail effect | O(t) where t = trail length | O(t) | Create element for each trail point |
| Cleanup/destroy | O(1) | O(1) | Remove listener, clear references |
Key Optimization Notes:
- Throttling reduces effective events from O(n) to O(n/k) where k is throttle factor
- Using requestAnimationFrame syncs updates with 60fps render cycle (~16ms)
- Map-based heatmap generation is more efficient than array: O(1) lookup vs O(n)
- Passive event listeners don't affect time complexity but improve scroll performance
| Feature | Use Case | Key Technique |
|---|---|---|
| Position display | Debug, dev tools | clientX/Y with fixed positioning |
| Custom cursor | Branding, UX enhancement | Hide default cursor, position element at coordinates |
| Spotlight effect | Focus attention, presentations | Overlay with radial gradient at cursor |
| Trail effect | Visual feedback, drawing apps | Array of recent positions with fade effect |
| Heatmap | Analytics, UX research | Grid-based bucketing of positions |
| Speed tracking | Gesture detection, gaming | Calculate distance between recent points |
| Drawing canvas | Signature, whiteboard | Track mousedown/move/up states |
-
Always throttle or use RAF: Mousemove fires 60-100x/sec. Throttle to 16ms (60fps) or 50ms (20fps) for complex operations. Use
requestAnimationFramefor visual updates. -
Clean up event listeners: Memory leaks occur when listeners persist after component unmount. Always return cleanup function in React useEffect or call
removeEventListenerin destroy methods. -
Match coordinates to positioning: Use
clientX/Ywithfixedpositioning,pageX/Ywithabsolutepositioning. Mismatches cause position errors when scrolling. -
Optimize for performance: Use
pointer-events: noneon tracking elements,transforminstead oftop/left, passive event listeners, and batch API calls instead of sending individually. -
Handle edge cases: Account for touch devices (touchmove events), cursor leaving window (mouseleave), scroll offset differences (clientY vs pageY), and high-frequency event floods.
- MDN: MouseEvent API - Complete reference for mouse event properties
- MDN: Touch Events - Handling touch input on mobile devices
- Web.dev: Optimize Input Delay - Performance optimization for input events
- JavaScript Event Loop (
js/general-concepts/event_loop.md) - Understanding event processing - Debounce & Throttle Implementation (
js/utils/debounce_throttle.md) - Performance optimization patterns
- clientX/Y includes scroll offset, pageX/Y does not
- clientX/Y is relative to viewport, pageX/Y is relative to document (includes scroll)
- They are identical and can be used interchangeably
- clientX/Y works only on mobile, pageX/Y only on desktop
- It improves performance by reducing event calculations
- It makes the element invisible to screen readers
- It allows mouse events to pass through to elements beneath
- It enables touch events on the element
- Use setTimeout to delay processing
- Increase the polling interval to 1 second
- Throttle the event handler to limit processing frequency (e.g., 16ms for 60fps)
- Disable tracking when the user is scrolling