کارایی و بهینهسازی
کارایی جاوا اسکریپت، مدیریت حافظه و Memory Leak، بهینهسازی DOM و Event با Debouncing و Throttling، Lazy Loading، Code Splitting، Performance API و Core Web Vitals.
چرا کارایی مهم است؟
کاربران از صفحهای که بیش از ۳ ثانیه طول بکشد ناراضیاند — سرعت = تجربه + سئو + نرخ تبدیل:
اهداف:
- First Paint < 1s
- Interactive < 3s
- 60fps انیمیشن (16ms per frame)
- بدون jank (گرفتگی)
اندازهگیری — قبل از بهینهسازی
// console.time:
console.time("عملیات");
heavyOperation();
console.timeEnd("عملیات"); // عملیات: 152ms
// performance.now — دقت میکروثانیه:
const start = performance.now();
heavyOperation();
const end = performance.now();
console.log(`زمان: ${(end - start).toFixed(2)}ms`);
// Performance API — زمانبندی صفحات:
const nav = performance.getEntriesByType("navigation")[0];
console.log("DOM آماده:", nav.domContentLoadedEventEnd);
console.log("بارگذاری کامل:", nav.loadEventEnd);
// Resource Timing — هر فایل:
performance.getEntriesByType("resource").forEach(r => {
console.log(r.name, r.duration.toFixed(0) + "ms");
});
مدیریت حافظه
// جاوا اسکریپت GC خودکار دارد — اما کمکش کنید:
// نشتی ۱ — متغیرهای فراموششده:
function process() {
const bigData = new Array(1000000).fill("داده");
// bigData بعد از return زنده میماند اگر بسته شود:
return function() {
return bigData.length; // نگهداشتن!
};
}
// نشتی ۲ — تایمرها:
const timer = setInterval(() => {...}, 1000);
// فراموش کردن clearInterval → نشتی!
// نشتی ۳ — شنوندههای رویداد:
element.addEventListener("click", handler);
// حذف عنصر بدون removeEventListener → نشتی!
// پاکسازی درست:
function createWidget(element) {
const handler = () => console.log("کلیک");
element.addEventListener("click", handler);
// تابع پاکسازی برگردان:
return () => {
element.removeEventListener("click", handler);
};
}
const cleanup = createWidget(el);
// بعد از استفاده:
cleanup(); // بدون نشتی
بهینهسازی DOM
// بد — دستکاری مکرر: ❌
for (let i = 0; i < 1000; i++) {
document.body.innerHTML += `<div>${i}</div>`; // ۱۰۰۰ reflow!
}
// خوب — یکبار: ✓
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // یک reflow!
// خواندن و نوشتن جداگانه:
// بد — خواندن/نوشتن متناوب:
el.style.width = "100px";
const h = el.offsetHeight; // reflow اجباری!
el.style.height = h + "px";
// خوب — همه خواندنها اول:
const height = el.offsetHeight; // یک reflow
el.style.width = "100px";
el.style.height = height + "px";
Debouncing — لرزشگیری
صبر تا توقف کاربر — برای جستجو، resize:
function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// استفاده — جستجو فقط بعد از توقف تایپ:
searchInput.addEventListener("input", debounce((e) => {
console.log("جستجو برای:", e.target.value);
}, 500));
// resize:
window.addEventListener("resize", debounce(() => {
console.log("اندازه جدید:", window.innerWidth);
}, 250));
Throttling — محدودسازی نرخ
حداکثر یکبار در بازه — برای scroll، mousemove:
function throttle(fn, limit = 200) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// اسکرول — حداکثر ۵ بار در ثانیه:
window.addEventListener("scroll", throttle(() => {
console.log("اسکرول:", window.scrollY);
}, 200));
// mousemove:
canvas.addEventListener("mousemove", throttle(draw, 16)); // ~60fps
Lazy Loading — بارگذاری تنبل
// تصاویر — native:
// <img src="placeholder.jpg" data-src="real.jpg" loading="lazy">
// با IntersectionObserver:
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll("img[data-src]").forEach(img => {
imageObserver.observe(img);
});
// محتوا — بارگذاری بخشها:
function lazySection(selector, loader) {
const el = document.querySelector(selector);
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
observer.disconnect();
el.innerHTML = "در حال بارگذاری...";
el.innerHTML = await loader();
}
}, { rootMargin: "200px" }); // قبل از رسیدن
observer.observe(el);
}
Code Splitting — تقسیم کد
// بهجای همه چیز در یک فایل:
// import { Chart } from "chart.js"; // ۲۰۰KB — همیشه؟
// فقط وقتی لازم است:
async function showChart() {
const { Chart } = await import("chart.js"); // دانلود لحظهای
new Chart(ctx, data);
}
// مسیریابی — هر صفحه جدا:
const routes = {
"/": () => import("./pages/home.js"),
"/dashboard": () => import("./pages/dashboard.js"),
"/settings": () => import("./pages/settings.js")
};
async function navigate(path) {
const page = await routes[path]();
document.querySelector("#app").innerHTML = page.default;
}
Rendering Performance
// انیمیشن فقط با transform و opacity:
// خوب — GPU: ✓
element.style.transform = "translateX(100px)";
element.style.opacity = "0.5";
// بد — CPU و reflow: ❌
element.style.left = "100px"; // layout
element.style.width = "200px"; // layout
// will-change — به مرورگر خبر بده:
element.style.willChange = "transform";
// requestAnimationFrame برای انیمیشن JS:
function animate() {
element.style.transform = `translateX(${x++}px)`;
requestAnimationFrame(animate);
}
// Web Workers برای محاسبه:
// سنگین → Worker → نتیجه → UI
PerformanceObserver — نظارت
// مشاهده متریکها:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.duration);
}
});
observer.observe({ entryTypes: ["measure", "navigation", "resource"] });
// اندازهگیری دستی:
performance.mark("start");
heavyOperation();
performance.mark("end");
performance.measure("عملیات سنگین", "start", "end");
const measures = performance.getEntriesByName("عملیات سنگین");
console.log(measures[0].duration);
Core Web Vitals
| متریک | معنی | هدف |
|---|---|---|
| LCP | بزرگترین محتوای قابل دید | < 2.5s |
| FID | اولین تعامل — تأخیر | < 100ms |
| CLS | جابجایی ناخواسته چیدمان | < 0.1 |
// اندازهگیری LCP:
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log("LCP:", lastEntry.startTime);
}).observe({ type: "largest-contentful-paint", buffered: true });
// CLS:
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) clsValue += entry.value;
}
console.log("CLS:", clsValue);
}).observe({ type: "layout-shift", buffered: true });
Lighthouse
ابزار گوگل برای ممیزی کارایی:
- Chrome DevTools → Lighthouse tab
- امتیاز ۰-۱۰۰ برای Performance، SEO، Accessibility
- پیشنهادهای مشخص بهبود
- CI/CD خودکار با Lighthouse CI
بهینهسازیهای کلیدی
| مشکل | راهحل |
|---|---|
| JS سنگین اولیه | Code Splitting + Tree Shaking |
| رویداد مکرر | Debounce/Throttle |
| تصاویر سنگین | Lazy Load + فرمت WebP |
| reflow زیاد | DocumentFragment + batch |
| محاسبه سنگین | Web Worker |
| نشتی حافظه | cleanup function |
این مبحث بخشی از دوره آموزش کامل جاوا اسکریپت است.