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.
1οΈβ£ What is Cursor Tracking?
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.
2οΈβ£ Why Use Cursor Tracking? / Why Does This Matter?
| 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
3οΈβ£ How It Works β Basic Implementation
Simple Cursor Position Display
// 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 tracker
π Dry Run: Cursor Position Tracking
Scenario: 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.
4οΈβ£ Understanding Key Concepts
Why Each Part Matters
1. Event Listener on document vs element
// β 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.
2. clientX/Y vs pageX/Y vs screenX/Y
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 used
What 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.
3. pointer-events: none on tracking elements
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.
4. Cleanup/Removal
// 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.
5. Edge Cases
- 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
5οΈβ£ Production/Advanced Implementation
Complete Cursor Tracker with Throttling & Analytics
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();
Additional Features
Spotlight Effect Implementation
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 remove
6οΈβ£ Real-World Examples
React Implementation with Hooks
import { 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 };
Trail Effect Component
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 }} /> ))} </> ); };
7οΈβ£ Common Interview Questions
Q1: How would you optimize cursor tracking for performance?
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
Q2: What's the difference between clientX, pageX, and screenX?
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 offset
Q3: How would you implement cursor tracking that works on touch devices?
Answer: 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 });
Q4: How would you prevent memory leaks in cursor tracking?
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
Q5: How would you implement a heatmap from cursor tracking data?
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); }); } }
Q6: How would you track cursor speed and acceleration?
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); } }
8οΈβ£ Common Pitfalls
Pitfall 1: Not Throttling Events
β 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);
Pitfall 2: Forgetting to Clean Up Event Listeners
β 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>; };
Pitfall 3: Using Wrong Coordinate System
β 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`; });
Pitfall 4: Blocking Interactions with Tracking Elements
β 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);
9οΈβ£ Time & Space Complexity
| 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
π Summary
Quick Reference Table
| 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 |
5 Key Takeaways
-
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.
π Further Reading
- 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
Test your understanding with 3 quick questions