بهینهسازی عملکرد جاوااسکریپت در پروژههای بزرگ
در پروژههای بزرگ جاوااسکریپت، بهینهسازی عملکرد از اهمیت ویژهای برخوردار است. در این مقاله، تکنیکهای پیشرفتهای را بررسی میکنیم که میتوانند سرعت اجرای کدهای شما را به طور چشمگیری افزایش دهند.
فهرست مطالب
۱. Debounce و Throttle برای Event Handlers
Eventهایی مانند scroll، resize و input میتوانند با فرکانس بالا اجرا شوند. استفاده از Debounce و Throttle میتواند تعداد اجرای کد را کاهش دهد.
مثال: پیادهسازی Debounce
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// استفاده
const searchInput = document.getElementById('search');
const performSearch = debounce((query) => {
// عملیات جستجو
console.log('Searching for:', query);
}, 300);
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
// پیادهسازی Throttle
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
نکته: از Debounce برای عملیاتهایی که باید پس از توقف کاربر انجام شوند استفاده کنید (مانند جستجو). از Throttle برای عملیاتهایی که باید با محدودیت فرکانس انجام شوند استفاده کنید (مانند هندلر اسکرول).
۲. Lazy Loading تصاویر و کدها
Lazy Loading باعث میشود منابع فقط زمانی که مورد نیاز هستند بارگیری شوند.
مثال: Lazy Loading تصاویر
// روش مدرن با Intersection Observer
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
img.classList.add('loaded');
observer.unobserve(img);
}
}
});
}, {
rootMargin: '50px',
threshold: 0.1
});
// استفاده
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// Lazy Loading برای کدهای JavaScript
const loadScript = (url) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.async = true;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Failed to load ${url}`));
document.head.appendChild(script);
});
};
// بارگیری شرطی
if (userNeedsFeature) {
loadScript('/path/to/feature.js')
.then(() => {
// ویژگی بارگیری شد
})
.catch(console.error);
}
۳. Code Splitting با Webpack
تقسیم کد به باندلهای کوچکتر باعث میشود فقط کدی که کاربر نیاز دارد بارگیری شود.
مثال: Dynamic Imports
// بارگیری پویای ماژولها
const loadDashboard = async () => {
try {
// بارگیری فقط در صورت نیاز
const dashboardModule = await import('./dashboard.js');
dashboardModule.initialize();
} catch (error) {
console.error('Failed to load dashboard:', error);
}
};
// بارگیری بر اساس شرایط
if (user.isAdmin) {
loadDashboard();
}
// تقسیم کد برای React با React.lazy
const AdminPanel = React.lazy(() => import('./AdminPanel'));
const UserProfile = React.lazy(() => import('./UserProfile'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
{user.role === 'admin' ? <AdminPanel /> : <UserProfile />}
</Suspense>
);
}
۴. Memoization برای توابع پرهزینه
Memoization تکنیکی است که خروجی توابع پرهزینه را ذخیره میکند تا از محاسبات تکراری جلوگیری کند.
مثال: تابع Memoize عمومی
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Returning cached result');
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// تابع پرهزینه
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// استفاده از Memoization
const memoizedFibonacci = memoize(fibonacci);
console.time('First call');
console.log(memoizedFibonacci(40)); // محاسبه میشود
console.timeEnd('First call');
console.time('Second call');
console.log(memoizedFibonacci(40)); // از کش برگردانده میشود
console.timeEnd('Second call');
// استفاده در React با useMemo و useCallback
function ExpensiveComponent({ data }) {
const processedData = useMemo(() => {
// پردازش سنگین دادهها
return data.map(item => ({
...item,
processed: heavyComputation(item)
}));
}, [data]); // فقط زمانی که data تغییر کند مجدداً محاسبه میشود
const handleClick = useCallback(() => {
// هندلر رویداد
console.log('Clicked:', processedData);
}, [processedData]);
return <button onClick={handleClick}>Click me</button>;
}
۵. استفاده از Web Workers برای پردازشهای سنگین
Web Workers امکان اجرای کد JavaScript در threadهای جداگانه را فراهم میکنند.
مثال: پردازش تصویر با Web Worker
// worker.js
self.onmessage = function(e) {
const { imageData, filter } = e.data;
const processedData = applyFilter(imageData, filter);
self.postMessage(processedData);
};
function applyFilter(imageData, filter) {
// پردازش سنگین تصویر
// ...
return processedImageData;
}
// main.js
const imageWorker = new Worker('image-worker.js');
function processImage(imageData, filter) {
return new Promise((resolve, reject) => {
imageWorker.onmessage = (e) => resolve(e.data);
imageWorker.onerror = reject;
imageWorker.postMessage({ imageData, filter });
});
}
// استفاده
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
processImage(imageData, 'grayscale')
.then(processedData => {
ctx.putImageData(processedData, 0, 0);
})
.catch(console.error);
// تمیز کردن Worker
window.addEventListener('beforeunload', () => {
imageWorker.terminate();
});
۶. Virtual DOM و Reconciliation در React
درک نحوه کار Virtual DOM میتواند به بهینهسازی رندرهای React کمک کند.
مثال: بهینهسازی رندر در React
// استفاده از React.memo برای جلوگیری از رندرهای غیرضروری
const UserList = React.memo(({ users, onSelect }) => {
console.log('UserList rendered');
return (
<ul>
{users.map(user => (
<UserItem
key={user.id}
user={user}
onSelect={onSelect}
/>
))}
</ul>
);
});
// استفاده از keyهای مناسب
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
// استفاده از id به جای index
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// بهینهسازی با useMemo و useCallback
function ProductList({ products, filters }) {
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter(product =>
filters.every(filter => filter(product))
);
}, [products, filters]);
const handleProductClick = useCallback((productId) => {
// هندلر رویداد
}, []);
return (
<div>
{filteredProducts.map(product => (
<Product
key={product.id}
product={product}
onClick={handleProductClick}
/>
))}
</div>
);
}
۷. Event Delegation برای کارایی بهتر
Event Delegation به جای اضافه کردن event listener به هر المان، یک listener به والد اضافه میکند.
مثال: Event Delegation برای لیست پویا
// روش ناکارآمد
function addListenersToItems() {
document.querySelectorAll('.list-item').forEach(item => {
item.addEventListener('click', handleItemClick);
});
}
// روش کارآمد با Event Delegation
document.getElementById('list').addEventListener('click', (e) => {
// بررسی آیا کلیک روی یک آیتم بوده
if (e.target.closest('.list-item')) {
const item = e.target.closest('.list-item');
const itemId = item.dataset.id;
handleItemClick(itemId);
}
// یا بررسی با event bubbling
if (e.target.matches('.list-item, .list-item *')) {
const item = e.target.closest('.list-item');
handleItemClick(item);
}
});
// برای Eventهای پویا
function handleDynamicEvents(container, eventName, selector, handler) {
container.addEventListener(eventName, function(e) {
let target = e.target;
// پیمایش به بالا تا پیدا کردن المان منطبق
while (target && target !== this) {
if (target.matches(selector)) {
handler.call(target, e);
break;
}
target = target.parentNode;
}
});
}
// استفاده
handleDynamicEvents(
document.getElementById('dynamic-list'),
'click',
'.dynamic-item',
function(e) {
console.log('Clicked:', this.dataset.id);
}
);
۸. استفاده از requestAnimationFrame برای انیمیشنها
requestAnimationFrame انیمیشنهای نرمتر و بهینهتری نسبت به setTimeout یا setInterval ایجاد میکند.
مثال: انیمیشن با requestAnimationFrame
// انیمیشن اسکرول نرم
function smoothScrollTo(targetPosition, duration = 1000) {
const startPosition = window.pageYOffset;
const distance = targetPosition - startPosition;
let startTime = null;
function animation(currentTime) {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / duration, 1);
// تابع easing
const easeInOutCubic = t =>
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const run = easeInOutCubic(progress);
window.scrollTo(0, startPosition + distance * run);
if (timeElapsed < duration) {
requestAnimationFrame(animation);
}
}
requestAnimationFrame(animation);
}
// انیمیشن المان
function animateElement(element, properties, duration) {
const startValues = {};
const changeValues = {};
Object.keys(properties).forEach(key => {
startValues[key] = parseFloat(getComputedStyle(element)[key]);
changeValues[key] = properties[key] - startValues[key];
});
let startTime = null;
function animation(currentTime) {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / duration, 1);
Object.keys(properties).forEach(key => {
const value = startValues[key] + changeValues[key] * progress;
element.style[key] = value + (key === 'opacity' ? '' : 'px');
});
if (timeElapsed < duration) {
requestAnimationFrame(animation);
}
}
requestAnimationFrame(animation);
}
۹. مدیریت حافظه و جلوگیری از Memory Leaks
Memory Leaks میتوانند باعث کاهش عملکرد و crash برنامه شوند.
مثال: جلوگیری از Memory Leaks
// ۱. تمیز کردن event listeners
class EventManager {
constructor() {
this.handlers = new Map();
}
addListener(element, event, handler) {
element.addEventListener(event, handler);
this.handlers.set(handler, { element, event, handler });
}
removeAllListeners() {
this.handlers.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this.handlers.clear();
}
}
// ۲. تمیز کردن intervals و timeouts
class TimerManager {
constructor() {
this.timers = new Set();
}
setInterval(callback, delay) {
const id = setInterval(callback, delay);
this.timers.add(id);
return id;
}
clearAll() {
this.timers.forEach(id => clearInterval(id));
this.timers.clear();
}
}
// ۳. جلوگیری از نگهداری referenceهای غیرضروری
function processLargeData(data) {
// پردازش دادههای بزرگ
const result = data.map(item => transform(item));
// حذف reference به دادههای اصلی
data = null;
return result;
}
// ۴. استفاده از WeakMap و WeakSet
const weakCache = new WeakMap();
function cacheExpensiveOperation(obj) {
if (weakCache.has(obj)) {
return weakCache.get(obj);
}
const result = expensiveOperation(obj);
weakCache.set(obj, result);
return result;
}
// ۵. تمیز کردن در React
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then(response => response.json())
.then(data => setData(data))
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
});
// Cleanup function
return () => {
controller.abort();
// تمیز کردن سایر منابع
};
}, []);
۱۰. مانیتورینگ و آنالیز عملکرد
مانیتورینگ مداوم عملکرد به شناسایی مشکلات کمک میکند.
مثال: ابزارهای مانیتورینگ
// ۱. اندازهگیری زمان اجرا
function measurePerformance(fn, ...args) {
const start = performance.now();
const result = fn(...args);
const end = performance.now();
console.log(`Function ${fn.name} took ${(end - start).toFixed(2)}ms`);
return result;
}
// ۲. مانیتورینگ Memory Usage
function logMemoryUsage(label = '') {
if (performance.memory) {
const used = performance.memory.usedJSHeapSize;
const total = performance.memory.totalJSHeapSize;
const limit = performance.memory.jsHeapSizeLimit;
console.log(`${label} Memory: ${(used / 1024 / 1024).toFixed(2)}MB / ${(total / 1024 / 1024).toFixed(2)}MB (Limit: ${(limit / 1024 / 1024).toFixed(2)}MB)`);
}
}
// ۳. استفاده از Performance API
function measureLongTask() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 50) { // تسکهای بیشتر از 50ms
console.warn('Long task detected:', entry);
// گزارش به سرور
reportPerformanceIssue(entry);
}
});
});
observer.observe({ entryTypes: ['longtask'] });
}
// ۴. مانیتورینگ Network Requests
function monitorNetwork() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
if (entry.duration > 1000) {
console.warn('Slow network request:', entry);
}
}
});
});
observer.observe({ entryTypes: ['resource'] });
}
// ۵. گزارش خطاهای عملکرد
window.addEventListener('error', (event) => {
const errorData = {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
// ارسال به سرور برای آنالیز
navigator.sendBeacon('/api/errors', JSON.stringify(errorData));
});
// ۶. مانیتورینگ FPS
let frameCount = 0;
let lastTime = performance.now();
function monitorFPS() {
frameCount++;
const currentTime = performance.now();
if (currentTime - lastTime >= 1000) {
const fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
if (fps < 30) {
console.warn(`Low FPS: ${fps}`);
// اقدامات بهینهسازی
}
frameCount = 0;
lastTime = currentTime;
}
requestAnimationFrame(monitorFPS);
}
// شروع مانیتورینگ FPS
monitorFPS();
نتیجهگیری
بهینهسازی عملکرد جاوااسکریپت فرآیندی مستمر است که نیاز به درک عمیق از نحوه کار موتورهای JavaScript دارد. با استفاده از تکنیکهای ارائه شده در این مقاله میتوانید عملکرد پروژههای خود را به طور چشمگیری بهبود بخشید.
همیشه به یاد داشته باشید: اندازهگیری قبل از بهینهسازی. ابتدا با ابزارهایی مانند Chrome DevTools مشکلات عملکرد را شناسایی کنید، سپس بهینهسازیهای هدفمند را اعمال کنید.