阿里云主机折上折
  • 微信号
Current Site:Index > Performance optimization for touch events

Performance optimization for touch events

Author:Chuan Chen 阅读数:19485人阅读 分类: 性能优化

Understanding the Basic Mechanism of Touch Events

Touch events play a critical role in mobile web development, but improper handling can lead to severe performance issues. Browser processing of touch events typically involves three phases: the capture phase, target phase, and bubbling phase. When a user touches the screen, the browser first identifies the touch target and then propagates the event according to the DOM tree structure.

document.addEventListener('touchstart', function(e) {
  console.log('Touch started at:', e.touches[0].clientX, e.touches[0].clientY);
}, false);

Native touch events include touchstart, touchmove, touchend, and touchcancel. Each event object contains properties such as touches (all current touch points), targetTouches (touch points on the current element), and changedTouches (touch points that changed relative to the last event).

Analysis of Common Performance Issues

Excessive event triggering is the primary issue. During rapid swiping, the touchmove event can trigger hundreds of times per second:

let count = 0;
element.addEventListener('touchmove', () => {
  count++;
  console.log(`Event fired ${count} times`);
});

Long execution times of event handlers can block the main thread. The Performance panel in Chrome DevTools can clearly observe this:

// Poor performance example
element.addEventListener('touchmove', () => {
  const start = performance.now();
  while(performance.now() - start < 16) {} // Simulate time-consuming operation
});

Memory leaks are also common in touch event handling, especially when event listeners are not properly removed:

// Incorrect example: May cause memory leaks
class Component {
  constructor() {
    this.element.addEventListener('touchstart', this.handleTouch);
  }
  
  handleTouch() {
    console.log('Touched');
  }
}

Event Delegation Optimization Strategy

Event delegation can significantly reduce the number of event listeners. Compare two implementation approaches:

// Traditional approach: Each child element has a listener
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('touchstart', handleItemTouch);
});

// Delegation approach: Single parent element listens
document.querySelector('.container').addEventListener('touchstart', function(e) {
  if(e.target.classList.contains('item')) {
    handleItemTouch(e);
  }
});

For dynamic content, delegation offers even greater advantages:

// Dynamic list handling
listContainer.addEventListener('touchstart', function(e) {
  const item = e.target.closest('.list-item');
  if(item) {
    // Handle specific item
  }
});

Throttling and Debouncing Techniques

Different throttling solutions for high-frequency events:

Basic throttling implementation:

function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function() {
    const context = this;
    const args = arguments;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
        if ((Date.now() - lastRan) >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

element.addEventListener('touchmove', throttle(handleMove, 100));

requestAnimationFrame-based throttling is more suitable for animation scenarios:

let ticking = false;
element.addEventListener('touchmove', function(e) {
  if (!ticking) {
    requestAnimationFrame(function() {
      handleMove(e);
      ticking = false;
    });
    ticking = true;
  }
});

Debouncing implementation differences:

function debounce(func, delay) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), delay);
  };
}

// Suitable for button-click operations
button.addEventListener('touchend', debounce(handleClick, 300));

Passive Event Listeners

Modern browsers support passive event listeners to optimize scrolling performance:

// Traditional approach may block scrolling
document.addEventListener('touchmove', preventDefault, { passive: false });

// Optimized approach
document.addEventListener('touchmove', preventDefault, { passive: true });

Detecting passive support:

let passiveSupported = false;
try {
  const options = {
    get passive() {
      passiveSupported = true;
      return false;
    }
  };
  window.addEventListener("test", null, options);
  window.removeEventListener("test", null, options);
} catch(err) {
  passiveSupported = false;
}

Touch Feedback Optimization Techniques

Immediate visual feedback is crucial for user experience. Avoid directly modifying DOM properties:

// Poor practice
element.addEventListener('touchstart', function() {
  this.style.backgroundColor = '#eee'; // Triggers repaint
});

// Optimized practice: Use CSS classes
.active {
  background-color: #eee;
  transform: translateZ(0); // Triggers hardware acceleration
}

Hardware acceleration example:

// Enable GPU acceleration for animated elements
.animated-element {
  will-change: transform;
  transform: translateZ(0);
}

Complex Gesture Handling Optimization

Performance considerations when implementing custom gestures:

class GestureDetector {
  constructor(element) {
    this.startX = 0;
    this.startY = 0;
    this.element = element;
    this.bindEvents();
  }
  
  bindEvents() {
    this.element.addEventListener('touchstart', this.handleStart.bind(this), { passive: true });
    this.element.addEventListener('touchmove', this.handleMove.bind(this), { passive: true });
    this.element.addEventListener('touchend', this.handleEnd.bind(this));
  }
  
  handleStart(e) {
    this.startX = e.touches[0].clientX;
    this.startY = e.touches[0].clientY;
  }
  
  handleMove(e) {
    const dx = e.touches[0].clientX - this.startX;
    const dy = e.touches[0].clientY - this.startY;
    
    if(Math.abs(dx) > Math.abs(dy)) {
      // Horizontal swipe handling
      this.handleHorizontalSwipe(dx);
    } else {
      // Vertical swipe handling
      this.handleVerticalSwipe(dy);
    }
  }
  
  handleHorizontalSwipe(delta) {
    // Use transform to avoid layout thrashing
    this.element.style.transform = `translateX(${delta}px)`;
  }
}

Memory Management and Event Cleanup

Correct event listener cleanup patterns:

class TouchComponent {
  constructor() {
    this.boundHandleTouch = this.handleTouch.bind(this);
    this.element.addEventListener('touchstart', this.boundHandleTouch);
  }
  
  handleTouch() {
    // Handling logic
  }
  
  destroy() {
    this.element.removeEventListener('touchstart', this.boundHandleTouch);
  }
}

WeakMap-assisted memory management:

const listenerMap = new WeakMap();

function addSafeListener(element, type, handler) {
  const wrappedHandler = function(e) {
    handler.call(this, e);
  };
  listenerMap.set(handler, wrappedHandler);
  element.addEventListener(type, wrappedHandler);
}

function removeSafeListener(element, type, handler) {
  const wrappedHandler = listenerMap.get(handler);
  if(wrappedHandler) {
    element.removeEventListener(type, wrappedHandler);
    listenerMap.delete(handler);
  }
}

Cross-Platform Compatibility Handling

Handling event differences across devices:

function getSupportedEvents() {
  if ('ontouchstart' in window) {
    return {
      start: 'touchstart',
      move: 'touchmove',
      end: 'touchend',
      cancel: 'touchcancel'
    };
  } else if ('onpointerdown' in window) {
    return {
      start: 'pointerdown',
      move: 'pointermove',
      end: 'pointerup',
      cancel: 'pointercancel'
    };
  } else {
    return {
      start: 'mousedown',
      move: 'mousemove',
      end: 'mouseup'
    };
  }
}

const events = getSupportedEvents();
element.addEventListener(events.start, handleStart);

Performance Monitoring and Debugging Tools

Using the Performance API for precise measurements:

function measureTouchPerformance() {
  const startMark = 'touch-start';
  const endMark = 'touch-end';
  
  performance.mark(startMark);
  
  element.addEventListener('touchend', function() {
    performance.mark(endMark);
    performance.measure('touch-duration', startMark, endMark);
    
    const measures = performance.getEntriesByName('touch-duration');
    console.log(`Touch duration: ${measures[0].duration}ms`);
    
    performance.clearMarks();
    performance.clearMeasures();
  }, { once: true });
}

Chrome DevTools' Event Listeners panel can inspect all attached listeners, helping to identify redundant event bindings.

Advanced Optimization Techniques

Using Web Workers for complex calculations:

// main.js
const worker = new Worker('touch-worker.js');

element.addEventListener('touchmove', function(e) {
  const touchData = {
    x: e.touches[0].clientX,
    y: e.touches[0].clientY,
    timestamp: e.timeStamp
  };
  worker.postMessage(touchData);
});

worker.onmessage = function(e) {
  updateUI(e.data.result);
};

// touch-worker.js
self.onmessage = function(e) {
  const data = e.data;
  // Perform complex calculations
  const result = heavyCalculation(data);
  self.postMessage({ result });
};

Intersection Observer optimization for invisible regions:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Add events when element is visible
      entry.target.addEventListener('touchmove', handleTouch);
    } else {
      // Remove events when element is invisible
      entry.target.removeEventListener('touchmove', handleTouch);
    }
  });
}, { threshold: 0.1 });

document.querySelectorAll('.scroll-item').forEach(item => {
  observer.observe(item);
});

Practical Case Analysis

Touch optimization for image galleries:

class Gallery {
  constructor(container) {
    this.container = container;
    this.startX = 0;
    this.currentX = 0;
    this.isDragging = false;
    this.animationId = null;
    this.items = Array.from(container.children);
    
    this.setupEvents();
    this.setupStyles();
  }
  
  setupStyles() {
    this.container.style.display = 'flex';
    this.container.style.overflow = 'hidden';
    this.container.style.touchAction = 'pan-y';
    this.items.forEach(item => {
      item.style.flexShrink = '0';
      item.style.width = '100%';
    });
  }
  
  setupEvents() {
    this.container.addEventListener('touchstart', this.handleStart.bind(this), { passive: true });
    this.container.addEventListener('touchmove', this.handleMove.bind(this), { passive: false });
    this.container.addEventListener('touchend', this.handleEnd.bind(this));
    
    // Responsive adjustments
    window.addEventListener('resize', this.handleResize.bind(this));
  }
  
  handleStart(e) {
    this.isDragging = true;
    this.startX = e.touches[0].clientX;
    this.currentX = this.getTranslateX();
    this.container.style.transition = 'none';
    cancelAnimationFrame(this.animationId);
  }
  
  handleMove(e) {
    if (!this.isDragging) return;
    
    const x = e.touches[0].clientX;
    const dx = x - this.startX;
    const newX = this.currentX + dx;
    
    // Limit drag range
    const maxX = 0;
    const minX = -(this.items.length - 1) * this.container.offsetWidth;
    
    if (newX > maxX + 50 || newX < minX - 50) {
      return;
    }
    
    this.setTranslateX(newX);
    e.preventDefault(); // Only prevent default behavior when necessary
  }
  
  handleEnd() {
    this.isDragging = false;
    this.container.style.transition = 'transform 0.3s ease-out';
    
    // Align to nearest slide
    const currentTranslate = this.getTranslateX();
    const currentSlide = Math.round(-currentTranslate / this.container.offsetWidth);
    const newTranslate = -currentSlide * this.container.offsetWidth;
    
    this.setTranslateX(newTranslate);
  }
  
  getTranslateX() {
    const transform = window.getComputedStyle(this.container).transform;
    if (!transform || transform === 'none') return 0;
    const matrix = transform.match(/^matrix\((.+)\)$/);
    return matrix ? parseFloat(matrix[1].split(', ')[4]) : 0;
  }
  
  setTranslateX(x) {
    this.container.style.transform = `translateX(${x}px)`;
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.