Design pattern practices in front-end performance optimization
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
下一篇:前端测试中的设计模式运用