阿里云主机折上折
  • 微信号
Current Site:Index > Design pattern practices in front-end performance optimization

Design pattern practices in front-end performance optimization

Author:Chuan Chen 阅读数:59836人阅读 分类: JavaScript

Performance Optimization Design Pattern Fundamentals

Design patterns play a key role in front-end performance optimization, providing proven solutions to common performance bottlenecks. Understanding how these patterns integrate with performance optimization is fundamental to building efficient web applications.

The Observer pattern is one of the core patterns for performance optimization. It reduces unnecessary rendering and computation by decoupling direct dependencies between components:

class Observable {
  constructor() {
    this.observers = [];
  }

  subscribe(fn) {
    this.observers.push(fn);
  }

  unsubscribe(fn) {
    this.observers = this.observers.filter(observer => observer !== fn);
  }

  notify(data) {
    this.observers.forEach(observer => observer(data));
  }
}

// Usage example
const resizeObservable = new Observable();
window.addEventListener('resize', () => {
  resizeObservable.notify({
    width: window.innerWidth,
    height: window.innerHeight
  });
});

Lazy Loading and Virtualization Patterns

The lazy loading pattern delays loading non-critical resources until they are actually needed. This is particularly effective when dealing with large lists or image collections:

class LazyLoader {
  constructor(selector, options = {}) {
    this.elements = document.querySelectorAll(selector);
    this.threshold = options.threshold || 0;
    this.init();
  }

  init() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.loadContent(entry.target);
          observer.unobserve(entry.target);
        }
      });
    }, { threshold: this.threshold });

    this.elements.forEach(el => observer.observe(el));
  }

  loadContent(element) {
    const src = element.getAttribute('data-src');
    if (src) {
      element.src = src;
    }
  }
}

// Usage example
new LazyLoader('img.lazy', { threshold: 0.1 });

Virtual scrolling is another performance optimization pattern that only renders elements within the viewport:

class VirtualScroll {
  constructor(container, itemHeight, totalItems, renderItem) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.totalItems = totalItems;
    this.renderItem = renderItem;
    this.visibleItems = Math.ceil(container.clientHeight / itemHeight);
    this.startIndex = 0;
    this.endIndex = this.startIndex + this.visibleItems;
    this.init();
  }

  init() {
    this.container.style.height = `${this.totalItems * this.itemHeight}px`;
    this.content = document.createElement('div');
    this.container.appendChild(this.content);
    this.updateItems();
    
    this.container.addEventListener('scroll', () => {
      this.startIndex = Math.floor(this.container.scrollTop / this.itemHeight);
      this.endIndex = Math.min(
        this.startIndex + this.visibleItems,
        this.totalItems
      );
      this.updateItems();
    });
  }

  updateItems() {
    const fragment = document.createDocumentFragment();
    for (let i = this.startIndex; i < this.endIndex; i++) {
      const item = document.createElement('div');
      item.style.position = 'absolute';
      item.style.top = `${i * this.itemHeight}px`;
      item.style.height = `${this.itemHeight}px`;
      this.renderItem(item, i);
      fragment.appendChild(item);
    }
    this.content.innerHTML = '';
    this.content.appendChild(fragment);
  }
}

Caching and Memoization Patterns

Caching is a classic technique for improving performance, and the memoization pattern is a function-level cache implementation:

function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Usage example
const expensiveCalculation = memoize((n) => {
  console.log('Calculating...');
  return n * n;
});

console.log(expensiveCalculation(5)); // Calculates and caches
console.log(expensiveCalculation(5)); // Reads from cache

For API requests, we can implement a more sophisticated caching strategy:

class ApiCache {
  constructor(maxAge = 300000) { // Default 5 minutes
    this.cache = new Map();
    this.maxAge = maxAge;
  }

  async get(url) {
    const cached = this.cache.get(url);
    const now = Date.now();
    
    if (cached && now - cached.timestamp < this.maxAge) {
      return cached.data;
    }
    
    const response = await fetch(url);
    const data = await response.json();
    this.cache.set(url, {
      data,
      timestamp: now
    });
    return data;
  }

  clear() {
    this.cache.clear();
  }
}

// Usage example
const apiCache = new ApiCache();
apiCache.get('https://api.example.com/data')
  .then(data => console.log(data));

Throttle and Debounce Patterns

For handling high-frequency events, throttle and debounce are essential performance optimization patterns:

function throttle(fn, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = new Date().getTime();
    if (now - lastCall < delay) return;
    lastCall = now;
    return fn.apply(this, args);
  };
}

function debounce(fn, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// Usage example
window.addEventListener('resize', throttle(() => {
  console.log('Resize event throttled');
}, 200));

const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(() => {
  console.log('Search input debounced');
}, 300));

A more advanced implementation can combine the benefits of both:

function adaptiveThrottle(fn, delay, options = {}) {
  let lastCall = 0;
  let timeoutId;
  const { leading = true, trailing = true } = options;
  
  return function(...args) {
    const now = Date.now();
    const remaining = delay - (now - lastCall);
    
    if (remaining <= 0) {
      if (leading) {
        lastCall = now;
        fn.apply(this, args);
      }
      clearTimeout(timeoutId);
    } else if (trailing && !timeoutId) {
      timeoutId = setTimeout(() => {
        lastCall = Date.now();
        timeoutId = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

Strategy Pattern and Performance Optimization

The strategy pattern allows selecting algorithms at runtime, which is particularly useful for optimizing performance based on different scenarios:

class SortStrategy {
  constructor(strategy = 'default') {
    this.strategies = {
      default: array => array.slice().sort((a, b) => a - b),
      quick: this.quickSort,
      merge: this.mergeSort,
      bubble: this.bubbleSort
    };
    this.setStrategy(strategy);
  }

  setStrategy(strategy) {
    this.currentStrategy = this.strategies[strategy] || this.strategies.default;
  }

  sort(array) {
    return this.currentStrategy(array);
  }

  quickSort(array) {
    if (array.length <= 1) return array;
    const pivot = array[0];
    const left = [];
    const right = [];
    
    for (let i = 1; i < array.length; i++) {
      if (array[i] < pivot) {
        left.push(array[i]);
      } else {
        right.push(array[i]);
      }
    }
    
    return [...this.quickSort(left), pivot, ...this.quickSort(right)];
  }

  // Other sorting algorithm implementations...
}

// Usage example
const sorter = new SortStrategy();
const smallArray = [3, 1, 4, 2];
sorter.setStrategy('bubble');
console.log(sorter.sort(smallArray));

const largeArray = Array.from({ length: 10000 }, () => Math.random());
sorter.setStrategy('quick');
console.log(sorter.sort(largeArray));

Proxy Pattern and Performance Optimization

The proxy pattern controls access to objects, enabling optimizations like lazy initialization and access control:

class HeavyObject {
  constructor() {
    console.log('Creating heavy object...');
    // Simulate time-consuming initialization
    this.data = Array.from({ length: 1000000 }, (_, i) => i);
  }

  getItem(index) {
    return this.data[index];
  }
}

class HeavyObjectProxy {
  constructor() {
    this.realObject = null;
  }

  getItem(index) {
    if (!this.realObject) {
      this.realObject = new HeavyObject();
    }
    return this.realObject.getItem(index);
  }
}

// Usage example
const proxy = new HeavyObjectProxy();
console.log(proxy.getItem(100)); // Only now creates the real object

For image loading, we can implement a smarter image proxy:

class ImageProxy {
  constructor(placeholderSrc, realSrc) {
    this.placeholder = new Image();
    this.placeholder.src = placeholderSrc;
    this.realSrc = realSrc;
    this.realImage = null;
    this.loaded = false;
  }

  loadRealImage() {
    if (this.loaded) return Promise.resolve();
    
    return new Promise((resolve) => {
      this.realImage = new Image();
      this.realImage.onload = () => {
        this.loaded = true;
        resolve();
      };
      this.realImage.src = this.realSrc;
    });
  }

  display(element) {
    element.appendChild(this.placeholder);
    
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        this.loadRealImage().then(() => {
          element.removeChild(this.placeholder);
          element.appendChild(this.realImage);
        });
        observer.disconnect();
      }
    });
    
    observer.observe(element);
  }
}

Flyweight Pattern and Object Pool

The flyweight pattern reduces memory usage by sharing objects, especially suitable for scenarios with many similar objects:

class FlyweightFactory {
  constructor() {
    this.flyweights = {};
  }

  getFlyweight(key) {
    if (!this.flyweights[key]) {
      this.flyweights[key] = new Flyweight(key);
    }
    return this.flyweights[key];
  }

  getCount() {
    return Object.keys(this.flyweights).length;
  }
}

class Flyweight {
  constructor(intrinsicState) {
    this.intrinsicState = intrinsicState;
  }

  operation(extrinsicState) {
    console.log(`Intrinsic: ${this.intrinsicState}, Extrinsic: ${extrinsicState}`);
  }
}

// Usage example
const factory = new FlyweightFactory();
const flyweight1 = factory.getFlyweight('shared');
const flyweight2 = factory.getFlyweight('shared');
flyweight1.operation('state1');
flyweight2.operation('state2');
console.log(factory.getCount()); // 1

The object pool pattern manages object lifecycles, avoiding performance overhead from frequent creation and destruction:

class ObjectPool {
  constructor(createFn, resetFn, size = 10) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.pool = [];
    this.size = size;
    this.initialize();
  }

  initialize() {
    for (let i = 0; i < this.size; i++) {
      this.pool.push(this.createFn());
    }
  }

  acquire() {
    if (this.pool.length > 0) {
      return this.pool.pop();
    }
    console.log('Creating new object - pool exhausted');
    return this.createFn();
  }

  release(obj) {
    this.resetFn(obj);
    this.pool.push(obj);
  }

  getPoolSize() {
    return this.pool.length;
  }
}

// Usage example
const pool = new ObjectPool(
  () => ({ id: Math.random().toString(36).substr(2, 9), used: false }),
  obj => { obj.used = false; },
  5
);

const objects = [];
for (let i = 0; i < 8; i++) {
  const obj = pool.acquire();
  obj.used = true;
  objects.push(obj);
}

console.log('Pool size during use:', pool.getPoolSize());

objects.forEach(obj => pool.release(obj));
console.log('Pool size after release:', pool.getPoolSize());

Command Pattern and Asynchronous Operations

The command pattern encapsulates operations, facilitating undo, redo, and asynchronous operation queues:

class CommandManager {
  constructor() {
    this.history = [];
    this.position = -1;
    this.executing = false;
    this.queue = [];
  }

  execute(command) {
    if (this.executing) {
      this.queue.push(command);
      return;
    }

    this.executing = true;
    command.execute().then(() => {
      // Trim history to current position
      this.history = this.history.slice(0, this.position + 1);
      this.history.push(command);
      this.position++;
      this.executing = false;
      
      if (this.queue.length > 0) {
        this.execute(this.queue.shift());
      }
    });
  }

  undo() {
    if (this.position >= 0) {
      this.history[this.position--].undo();
    }
  }

  redo() {
    if (this.position < this.history.length - 1) {
      this.history[++this.position].execute();
    }
  }
}

class AsyncCommand {
  constructor(executeFn, undoFn) {
    this.executeFn = executeFn;
    this.undoFn = undoFn;
  }

  execute() {
    return new Promise((resolve) => {
      setTimeout(() => {
        this.executeFn();
        resolve();
      }, 1000);
    });
  }

  undo() {
    this.undoFn();
  }
}

// Usage example
const manager = new CommandManager();
const command1 = new AsyncCommand(
  () => console.log('Executing command 1'),
  () => console.log('Undoing command 1')
);

const command2 = new AsyncCommand(
  () => console.log('Executing command 2'),
  () => console.log('Undoing command 2')
);

manager.execute(command1);
manager.execute(command2);

setTimeout(() => {
  manager.undo();
  manager.undo();
  manager.redo();
}, 3000);

State Pattern and Performance Optimization

The state pattern changes an object's behavior by altering its internal state, avoiding performance overhead from conditional branches:

class TrafficLight {
  constructor() {
    this.states = {
      red: new RedState(this),
      yellow: new YellowState(this),
      green: new GreenState(this)
    };
    this.currentState = this.states.red;
  }

  changeState(state) {
    this.currentState = this.states[state];
    this.currentState.activate();
  }

  show() {
    return this.currentState.show();
  }

  next() {
    this.currentState.next();
  }
}

class LightState {
  constructor(light, name) {
    this.light = light;
    this.name = name;
  }

  activate() {
    console.log(`Activating ${this.name} light`);
  }

  show() {
    return this.name;
  }
}

class RedState extends LightState {
  constructor(light) {
    super(light, 'red');
  }

  next() {
    this.light.changeState('green');
  }
}

class GreenState extends LightState {
  constructor(light) {
    super(light, 'green');
  }

  next() {
    this.light.changeState('yellow');
  }
}

class YellowState extends LightState {
  constructor(light) {
    super(light, 'yellow');
  }

  next() {
    this.light.changeState('red');
  }
}

// Usage example
const trafficLight = new TrafficLight();
console.log(trafficLight.show()); // red
trafficLight.next();
console.log(trafficLight.show()); // green
trafficLight.next();
console.log(trafficLight.show()); // yellow

Composite Pattern and Batch Operations

The composite pattern allows uniform handling of individual objects and composite objects, optimizing batch operation performance:

class DOMComponent {
  constructor(element) {
    this.element = element;
    this.children = [];
  }

  add(child) {
    this.children.push(child);
  }

  remove(child) {
    const index = this.children.indexOf(child);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }

  // Batch update styles
  updateStyle(styles) {
    // Use requestAnimationFrame to optimize batch DOM operations
    requestAnimationFrame(() => {
      this.applyStyleToSelf(styles);
      this.children.forEach(child => child.updateStyle(styles));
    });
  }

  applyStyleToSelf(styles) {
    Object.assign(this.element.style, styles);
  }

  // Batch add event listeners
  addEventListener(type, listener, options) {
    this.element.addEventListener(type, listener, options);
    this.children.forEach(child => child.addEventListener(type, listener, options));
  }
}

// Usage example
const container = new DOMComponent(document.getElementById('container'));
const header = new DOMComponent(document.createElement('div'));
const content = new DOMComponent(document.createElement('div'));

container.add(header);
container.add(content);

// Batch update styles for all child elements
container.updateStyle({
  color: 'red',
  fontSize: '16px'
});

// Batch add event listeners
container.addEventListener('click', () => {
  console.log('Clicked on container or child');
});

Decorator Pattern and Performance Monitoring

The decorator pattern dynamically adds functionality, making it ideal for non-intrusive performance monitoring:

function withPerformanceMetrics(fn, metricName) {
  return function(...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    const end = performance.now();
    
    const duration = end - start;
    console.log(`${metricName} took ${duration.toFixed(2)}ms`);
    
    if (duration > 100) {
      console.warn(`Performance warning: ${metricName} took too long`);
    }
    
    return result;
  };
}

// Usage example
class DataProcessor {
  @withPerformanceMetrics
  processLargeData(data) {
    // Simulate time-consuming operation
    let result = 0;
    for (let i = 0; i < data.length; i++) {
      result += data[i];
    }
    return result;
  }
}

// Or manually apply decorator

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

如果侵犯了你的权益请来信告知我们删除。邮箱: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 ☕.