أداء الويب وإمكانية الوصول
أنماط الأداء المتقدمة
بخلاف Core Web Vitals، يتوقع المحاورون معرفتك بتقنيات تحسين الأداء العملية. يغطي هذا الدرس الأنماط التي سيُطلب منك تنفيذها أو مناقشتها.
استراتيجيات تقسيم الكود
تقسيم الكود يقسم حزمة JavaScript إلى أجزاء أصغر تُحمّل حسب الطلب. بدونه، يُنزّل المستخدمون التطبيق بالكامل قبل رؤية أي شيء.
التقسيم على أساس المسارات
النهج الأكثر شيوعًا — كل مسار يُحمّل حزمته الخاصة:
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// كل مسار هو جزء منفصل
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
التقسيم على أساس المكونات
قسّم المكونات الثقيلة غير المرئية فورًا:
import { lazy, Suspense, useState } from 'react';
// مكتبة الرسوم البيانية الثقيلة تُحمّل فقط عند فتح التحليلات
const AnalyticsChart = lazy(() => import('./components/AnalyticsChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>لوحة التحكم</h1>
<button onClick={() => setShowChart(true)}>عرض التحليلات</button>
{showChart && (
<Suspense fallback={<div>جارٍ تحميل الرسم البياني...</div>}>
<AnalyticsChart />
</Suspense>
)}
</div>
);
}
import() الديناميكي للكود غير المتعلق بـ React
// تحميل مكتبة ثقيلة فقط عند الحاجة
async function handleExport() {
const { exportToPDF } = await import('./utils/pdfExport');
exportToPDF(document.getElementById('report'));
}
// تحميل ميزة مشروط
async function initEditor() {
if (userHasPermission('edit')) {
const { RichTextEditor } = await import('./components/RichTextEditor');
mountEditor(RichTextEditor);
}
}
تحسين الصور
التحميل الكسول الأصلي
<!-- المتصفح يتعامل مع التحميل الكسول أصليًا -->
<img src="/photo.jpg" alt="وصف" loading="lazy"
width="400" height="300">
<!-- لا تستخدم التحميل الكسول لصور فوق الطي -->
<img src="/hero.jpg" alt="البطل" loading="eager"
fetchpriority="high" width="1200" height="600">
صور متجاوبة مع srcset
<!-- المتصفح يختار الحجم المناسب بناءً على نافذة العرض -->
<img srcset="/photo-400.jpg 400w,
/photo-800.jpg 800w,
/photo-1200.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
src="/photo-800.jpg"
alt="صورة متجاوبة">
عنصر <picture> لاختيار التنسيق
<picture>
<!-- المتصفح يختار أول تنسيق مدعوم -->
<source srcset="/photo.avif" type="image/avif">
<source srcset="/photo.webp" type="image/webp">
<img src="/photo.jpg" alt="صورة مع تنسيق بديل">
</picture>
next/image في Next.js
import Image from 'next/image';
// تحسين تلقائي: تغيير الحجم، تحويل التنسيق، التحميل الكسول
<Image
src="/hero.jpg"
alt="صورة البطل"
width={1200}
height={600}
priority // تعطيل التحميل الكسول لصور LCP
placeholder="blur"
blurDataURL={blurHash}
/>
تلميحات الموارد: Prefetch و Preload و Preconnect
<!-- PRELOAD: مورد عالي الأولوية مطلوب للصفحة الحالية -->
<!-- استخدمه للخطوط والصور الحرجة و CSS فوق الطي -->
<link rel="preload" href="/fonts/main.woff2" as="font"
type="font/woff2" crossorigin>
<!-- PREFETCH: مورد منخفض الأولوية مطلوب للتنقل المستقبلي -->
<!-- استخدمه لحزم الصفحة التالية التي من المرجح أن يزورها المستخدم -->
<link rel="prefetch" href="/js/dashboard.chunk.js">
<!-- PRECONNECT: إنشاء اتصال مبكر بمصدر طرف ثالث -->
<!-- يوفر وقت بحث DNS + TCP + مصافحة TLS -->
<link rel="preconnect" href="https://api.example.com">
<!-- DNS-PREFETCH: حل DNS فقط (أخف من preconnect) -->
<link rel="dns-prefetch" href="https://analytics.example.com">
متى تستخدم كلًا منها:
| التلميح | الأولوية | حالة الاستخدام |
|---|---|---|
preload |
عالية | موارد الصفحة الحالية الحرجة |
prefetch |
منخفضة | موارد الصفحة التالية |
preconnect |
متوسطة | واجهات API لطرف ثالث ستستدعيها قريبًا |
dns-prefetch |
منخفضة | مصادر طرف ثالث للتحليلات والإعلانات |
Service Workers واستراتيجيات التخزين المؤقت
تعترض Service Workers طلبات الشبكة ويمكنها تقديم استجابات مخزنة مؤقتًا:
التخزين المؤقت أولاً (العمل دون اتصال أولاً)
الأفضل للأصول الثابتة التي نادرًا ما تتغير:
// service-worker.js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
// إرجاع النسخة المخزنة، العودة للشبكة كبديل
return cached || fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('static-v1').then((cache) => {
cache.put(event.request, clone);
});
return response;
});
})
);
});
الشبكة أولاً
الأفضل للبيانات الديناميكية التي يجب أن تكون حديثة:
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// تحديث التخزين المؤقت بالاستجابة الحديثة
const clone = response.clone();
caches.open('dynamic-v1').then((cache) => {
cache.put(event.request, clone);
});
return response;
})
.catch(() => {
// فشلت الشبكة، تقديم من التخزين المؤقت
return caches.match(event.request);
})
);
});
قديم أثناء إعادة التحقق
أفضل توازن بين السرعة والحداثة — تقديم المخزن، تحديث في الخلفية:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('swr-v1').then((cache) => {
return cache.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
// إرجاع المخزن فورًا، تحديث التخزين المؤقت في الخلفية
return cached || fetchPromise;
});
})
);
});
تحليل الحزمة
تحديد التبعيات الكبيرة
# محلل حزمة Webpack
npx webpack-bundle-analyzer stats.json
# محلل Next.js المدمج
# التثبيت: npm install @next/bundle-analyzer
# next.config.js:
# const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: true });
# module.exports = withBundleAnalyzer(nextConfig);
التبعيات الكبيرة الشائعة وبدائلها:
| المكتبة الثقيلة | الحجم | البديل الخفيف | الحجم |
|---|---|---|---|
moment.js |
~300 كيلوبايت | date-fns (قابلة لهز الشجرة) |
~10 كيلوبايت مستخدمة |
lodash |
~70 كيلوبايت | lodash-es + هز الشجرة |
~5 كيلوبايت مستخدمة |
chart.js كاملة |
~200 كيلوبايت | تحميل كسول عند التفاعل | 0 كيلوبايت أولية |
متى يساعد التخزين المؤقت (ومتى يضر)
هذا موضوع مفضل في المقابلات. كثير من المرشحين يفرطون في استخدام React.memo و useMemo و useCallback.
تكلفة التخزين المؤقت
كل تخزين مؤقت له تكلفة:
- الذاكرة: تخزين القيمة السابقة والتبعيات
- المقارنة: يجب على React مقارنة التبعيات في كل عرض
- التعقيد: كود أكثر للقراءة والصيانة وتتبع الأخطاء
متى تستخدم useMemo
// جيد: حساب مكلف يُبطئ كل عرض
function ProductList({ products, filter }) {
const filtered = useMemo(() => {
// تصفية 10,000 منتج مكلفة
return products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase()) &&
p.price >= filter.minPrice &&
p.category === filter.category
);
}, [products, filter]);
return <ul>{filtered.map(p => <ProductCard key={p.id} product={p} />)}</ul>;
}
// سيئ: حساب بسيط — المقارنة تكلف أكثر من الحساب
function UserGreeting({ name }) {
// لا تستخدم التخزين المؤقت هنا — ربط السلاسل النصية شبه فوري
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
return <h1>{greeting}</h1>;
}
متى تستخدم useCallback
// جيد: معالج مُمرر لمكون فرعي مُخزن يتحقق من المساواة المرجعية
const MemoizedList = React.memo(({ items, onItemClick }) => (
<ul>{items.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>{item.name}</li>
))}</ul>
));
function Parent({ items }) {
// بدون useCallback، يُعاد عرض MemoizedList كل مرة يُعرض Parent
// لأن onItemClick ستكون مرجع دالة جديد كل مرة
const handleClick = useCallback((id) => {
console.log('تم النقر:', id);
}, []);
return <MemoizedList items={items} onItemClick={handleClick} />;
}
// سيئ: لا يوجد مكون فرعي مُخزن — useCallback يضيف تكلفة بدون فائدة
function SearchForm() {
const [query, setQuery] = useState('');
// هذا useCallback لا فائدة منه — الإدخال غير مُخزن
const handleChange = useCallback((e) => {
setQuery(e.target.value);
}, []);
return <input value={query} onChange={handleChange} />;
}
قاعدة اتخاذ القرار
استخدم التخزين المؤقت فقط عندما تتحقق الشروط الثلاثة جميعها:
- قست مشكلة أداء (React DevTools Profiler)
- الحساب مكلف فعلاً أو المساواة المرجعية مهمة لمكون فرعي مُخزن
- التبعيات تتغير أقل من مرات عرض المكون
نصيحة للمقابلات: إذا سُئلت "متى تستخدم useMemo؟"، ابدأ بـ "سأقيس أولاً بـ React DevTools Profiler لتأكيد وجود مشكلة" — هذا يُظهر نضجًا.
التالي: سنغطي معايير إمكانية الوصول WCAG 2.2 وتقنيات الاختبار. :::