الأمان والأداء

مشروع تحسين الأداء (الجزء الأول)

20 دقيقة الدرس 32 من 35

مشروع تحسين الأداء (الجزء الأول)

تحسين الأداء هو عملية منهجية لتحديد الاختناقات وتنفيذ تحسينات مستهدفة. في هذا المشروع العملي، سنقوم بتدقيق تطبيق بطيء، وتحديد مشاكل الأداء، وتنفيذ تحسينات شاملة للواجهة الأمامية. سيحول هذا المشروع المكون من جزأين تطبيقًا بطيئًا إلى نظام عالي الأداء.

نظرة عامة على المشروع

سنقوم بتحسين تطبيق تجارة إلكترونية في العالم الحقيقي يعاني من مشاكل أداء كبيرة. التطبيق لديه وقت تحميل 7 ثوانٍ، ودرجات Lighthouse ضعيفة، ومستخدمين محبطين. هدفنا هو تحقيق أوقات تحميل أقل من ثانيتين ومقاييس أداء ممتازة.

أهداف المشروع:
- تقليل وقت تحميل الصفحة الأولية من 7 ثانية إلى <2 ثانية
- تحقيق درجة Lighthouse >90
- تنفيذ التخزين المؤقت الشامل
- تحسين الصور والأصول
- تحسين الأداء المدرك

التدقيق الأولي للأداء

قبل إجراء أي تغييرات، حدد مقاييس الأساس. استخدم أدوات متعددة للحصول على صورة كاملة:

<!-- قائمة التدقيق على الأداء -->

1. تدقيق Google Lighthouse:
# Chrome DevTools > Lighthouse
# تشغيل التدقيق في وضع التصفح الخفي
# اختبار على اتصالات 3G و4G

النتائج الأولية (قبل التحسين):
الأداء: 42/100
First Contentful Paint: 3.2s
Largest Contentful Paint: 7.1s
Time to Interactive: 8.5s
Total Blocking Time: 2,140ms
Cumulative Layout Shift: 0.18

2. تحليل WebPageTest:
# زيارة webpagetest.org
# اختبار من مواقع متعددة
# استخدام ملف تعريف اتصال 3G

النتائج الرئيسية:
- 127 طلب HTTP
- 4.2 ميجابايت إجمالي وزن الصفحة
- بدون ضغط نصي
- صور غير محسّنة (3.1 ميجابايت)
- موارد تمنع العرض
- بدون تخزين مؤقت للمتصفح

3. لوحة شبكة Chrome DevTools:
# فتح DevTools > Network
# تعطيل التخزين المؤقت
# تسجيل تحميل الصفحة الكامل

الملاحظات:
- 12 ملف CSS (320 كيلوبايت إجمالي)
- 15 ملف JavaScript (890 كيلوبايت إجمالي)
- 45 صورة (3.1 ميجابايت إجمالي)
- عدم استخدام CDN
- تحميل سكريبت متزامن

4. لوحة الأداء في Chrome DevTools:
# DevTools > Performance
# تسجيل تحميل الصفحة
# تحليل نشاط الخيط الرئيسي

الاختناقات المحددة:
- تنفيذ JavaScript طويل (2.1 ثانية)
- تخطيط مفرط
- سكريبتات طرف ثالث غير محسّنة
- حجم DOM كبير (1,842 عقدة)

تحديد أولويات التحسينات

بناءً على التدقيق، حدد أولويات التحسينات حسب التأثير والجهد:

<!-- مصفوفة أولوية التحسين -->

تأثير عالي، جهد منخفض:
✓ تمكين ضغط النص (gzip/brotli)
✓ تنفيذ التخزين المؤقت للمتصفح
✓ تصغير CSS وJavaScript
✓ تحسين الصور (تنسيقات حديثة، ضغط)
✓ إزالة الموارد التي تمنع العرض

تأثير عالي، جهد متوسط:
✓ تنفيذ تقسيم الكود
✓ التحميل الكسول للصور والمكونات
✓ تحسين مسار العرض الحرج
✓ تقليل تأثير سكريبتات الطرف الثالث

تأثير متوسط، جهد منخفض:
✓ إزالة CSS/JS غير المستخدمة
✓ تحسين خطوط الويب
✓ تنفيذ تلميحات الموارد
✓ تقليل تعقيد DOM

ترتيب التنفيذ:
1. تحسينات الواجهة الأمامية (الجزء 1)
2. تحسينات الخادم وقاعدة البيانات (الجزء 2)

الخطوة 1: تمكين ضغط النص

ضغط جميع الأصول النصية (HTML، CSS، JS، JSON) لتقليل حجم النقل:

<!-- تكوين Apache .htaccess -->

# تمكين ضغط gzip
<IfModule mod_deflate.c>
# ضغط HTML، CSS، JavaScript، Text، XML
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE text/plain
</IfModule>

<!-- تكوين Nginx -->

# تمكين ضغط gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
text/xml;

# تمكين brotli (إذا كان الوحدة متاحة)
brotli on;
brotli_comp_level 6;
brotli_types
text/plain
text/css
text/javascript
application/javascript
application/json;

النتائج:
- CSS: 320 كيلوبايت → 58 كيلوبايت (تخفيض 82%)
- JavaScript: 890 كيلوبايت → 246 كيلوبايت (تخفيض 72%)
- HTML: 42 كيلوبايت → 12 كيلوبايت (تخفيض 71%)
إجمالي التوفير: 936 كيلوبايت

الخطوة 2: تنفيذ التخزين المؤقت للمتصفح

تكوين التخزين المؤقت القوي للأصول الثابتة للقضاء على الطلبات الزائدة:

<!-- تكوين التخزين المؤقت لـ Apache -->

<IfModule mod_expires.c>
ExpiresActive On

# الصور
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"

# CSS وJavaScript
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"

# الخطوط
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"

# HTML (تخزين مؤقت أقصر)
ExpiresByType text/html "access plus 1 hour"
</IfModule>

<!-- رؤوس Cache-Control -->

<IfModule mod_headers.c>
# الأصول المُصدّرة - تخزين مؤقت قوي
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|css|js|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

# HTML - التحقق في كل طلب
<FilesMatch "\.html$">
Header set Cache-Control "public, max-age=0, must-revalidate"
</FilesMatch>
</IfModule>

النتائج:
- الزوار العائدون: 0 طلبات أصول (كلها مخزنة مؤقتًا)
- وقت تحميل الصفحة للزيارات العائدة: 7 ثانية → 1.2 ثانية

الخطوة 3: تحسين الصور

تمثل الصور 3.1 ميجابايت من وزن الصفحة البالغ 4.2 ميجابايت. تحسين الصور الشامل أمر بالغ الأهمية:

<!-- استراتيجية تحسين الصور -->

1. التحويل إلى تنسيقات حديثة:
# تثبيت sharp لـ Node.js
npm install sharp

# سكريبت التحويل (convert-images.js)
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');

async function convertToWebP(inputPath, outputPath) {
await sharp(inputPath)
.webp({ quality: 85 })
.toFile(outputPath);
}

async function convertToAVIF(inputPath, outputPath) {
await sharp(inputPath)
.avif({ quality: 75 })
.toFile(outputPath);
}

// معالجة جميع صور المنتجات
const imagesDir = './public/images/products';
fs.readdirSync(imagesDir).forEach(async (file) => {
if (file.match(/\.(jpg|jpeg|png)$/)) {
const inputPath = path.join(imagesDir, file);
const baseName = file.replace(/\.(jpg|jpeg|png)$/, '');

await convertToWebP(inputPath,
path.join(imagesDir, baseName + '.webp'));
await convertToAVIF(inputPath,
path.join(imagesDir, baseName + '.avif'));
}
});

2. تنفيذ الصور المستجيبة:
<!-- ترميز صورة مستجيبة حديثة -->
<picture>
<source
type="image/avif"
srcset="product-320.avif 320w,
product-640.avif 640w,
product-1280.avif 1280w"
sizes="(max-width: 640px) 100vw, 640px">
<source
type="image/webp"
srcset="product-320.webp 320w,
product-640.webp 640w,
product-1280.webp 1280w"
sizes="(max-width: 640px) 100vw, 640px">
<img
src="product-640.jpg"
srcset="product-320.jpg 320w,
product-640.jpg 640w,
product-1280.jpg 1280w"
sizes="(max-width: 640px) 100vw, 640px"
alt="اسم المنتج"
loading="lazy"
decoding="async">
</picture>

3. تحسين صور JPEG الأصلية:
# استخدام imagemagick
mogrify -strip -quality 85 -sampling-factor 4:2:0 *.jpg

4. إنشاء أحجام متعددة:
# سكريبت تغيير الحجم التلقائي
for img in *.jpg; do
convert $img -resize 320x product-320-${img}
convert $img -resize 640x product-640-${img}
convert $img -resize 1280x product-1280-${img}
done

النتائج:
- AVIF: 3.1 ميجابايت → 420 كيلوبايت (تخفيض 86%)
- WebP: 3.1 ميجابايت → 680 كيلوبايت (تخفيض 78%)
- احتياطي JPEG المحسّن: 3.1 ميجابايت → 1.1 ميجابايت (تخفيض 65%)
- مع التحميل الكسول: التحميل الأولي فقط 180 كيلوبايت
نصيحة احترافية: استخدم عنصر `<picture>` مع مصادر متعددة. ستختار المتصفحات الحديثة تلقائيًا أفضل تنسيق تدعمه (AVIF > WebP > JPEG).

الخطوة 4: تنفيذ التحميل الكسول

تأجيل تحميل الصور والمكونات خارج الشاشة حتى الحاجة إليها:

<!-- التحميل الكسول الأصلي -->

<!-- تحميل كسول بسيط للصور -->
<img src="product.jpg" alt="منتج" loading="lazy">

<!-- تحميل كسول متقدم مع Intersection Observer -->
<script>
// تحميل كسول للصور
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.srcset = img.dataset.srcset;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
}, {
rootMargin: '50px' // بدء التحميل 50 بكسل قبل الظهور
});

document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});

// تحميل كسول للمكونات
const componentObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const component = entry.target;
// تحميل كود المكون
import(component.dataset.component).then(module => {
module.init(component);
});
componentObserver.unobserve(component);
}
});
});

document.querySelectorAll('[data-component]').forEach(el => {
componentObserver.observe(el);
});
</script>

<!-- ترميز HTML للتحميل الكسول -->
<img
class="lazy"
data-src="product-full.jpg"
data-srcset="product-320.jpg 320w, product-640.jpg 640w"
src="placeholder.jpg"
alt="اسم المنتج">

النتائج:
- الصور الأولية المحمّلة: 45 → 6
- وزن الصفحة الأولي: 4.2 ميجابايت → 850 كيلوبايت
- تحسين LCP: 7.1 ثانية → 3.2 ثانية

الخطوة 5: إزالة الموارد التي تمنع العرض

إزالة الموارد التي تمنع عرض الصفحة الأولي:

<!-- الأصل (منع العرض) -->
<head>
<link rel="stylesheet" href="normalize.css">
<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="custom.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
</head>

<!-- المُحسّن (بدون منع) -->
<head>
<!-- CSS حرج مضمّن -->
<style>
/* CSS حرج فوق الطية (~14KB) */
body { margin: 0; font-family: system-ui; }
.header { /* أنماط الرأس */ }
.hero { /* أنماط قسم البطل */ }
</style>

<!-- تحميل مسبق للموارد الحرجة -->
<link rel="preload" href="main.css" as="style">
<link rel="preload" href="main.js" as="script">

<!-- تحميل غير متزامن لـ CSS غير الحرجة -->
<link rel="preload" href="main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="main.css"></noscript>
</head>
<body>
<!-- المحتوى -->

<!-- تأجيل JavaScript -->
<script src="main.js" defer></script>
</body>

<!-- استخراج CSS الحرج (باستخدام حزمة critical) -->
# تثبيت critical
npm install critical

# إنشاء CSS حرج
const critical = require('critical');

critical.generate({
inline: true,
base: 'public/',
src: 'index.html',
dest: 'index-critical.html',
width: 1300,
height: 900
});

النتائج:
- الموارد التي تمنع العرض: 5 → 0
- First Contentful Paint: 3.2 ثانية → 1.1 ثانية
- Largest Contentful Paint: 3.2 ثانية → 2.1 ثانية

الخطوة 6: تصغير وتجميع الأصول

تقليل أحجام الملفات وطلبات HTTP من خلال التصغير والتجميع:

<!-- تكوين Webpack -->

// webpack.config.js
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
vendor: ['jquery', 'bootstrap']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js'
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: { drop_console: true },
format: { comments: false }
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendor',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};

النتائج:
- ملفات JavaScript: 15 → 3 (main, vendor, common)
- ملفات CSS: 12 → 1
- إجمالي حجم JS: 246 كيلوبايت (مضغوط) → 198 كيلوبايت
- إجمالي حجم CSS: 58 كيلوبايت (مضغوط) → 42 كيلوبايت
- طلبات HTTP: 127 → 38

الخطوة 7: تنفيذ تقسيم الكود

تقسيم الكود إلى أجزاء أصغر يتم تحميلها عند الطلب:

<!-- تقسيم الكود على أساس المسار (مثال React) -->

import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

// تحميل كسول لمكونات المسار
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));

function App() {
return (
<Router>
<Suspense fallback={<div>جارٍ التحميل...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/products" component={Products} />
<Route path="/product/:id" component={ProductDetail} />
<Route path="/cart" component={Cart} />
<Route path="/checkout" component={Checkout} />
</Switch>
</Suspense>
</Router>
);
}

<!-- الاستيراد الديناميكي للميزات -->

// تحميل مكون modal فقط عند الحاجة
button.addEventListener('click', async () => {
const { Modal } = await import('./components/Modal');
const modal = new Modal();
modal.show();
});

النتائج:
- الحزمة الأولية: 890 كيلوبايت → 156 كيلوبايت
- جزء الصفحة الرئيسية: 156 كيلوبايت
- جزء صفحة المنتجات: 89 كيلوبايت (يتم تحميله عند الطلب)
- سير الدفع: 124 كيلوبايت (يتم تحميله عند الطلب)
- Time to Interactive: 8.5 ثانية → 3.1 ثانية
تمرين عملي: قم بتحسين تطبيق نموذجي:

1. قم بتنزيل مستودع slow-ecommerce-starter
2. قم بتشغيل تدقيق Lighthouse ووثق مقاييس الأساس
3. قم بتمكين ضغط النص على خادمك
4. قم بتحويل 5 صور منتجات إلى WebP وAVIF
5. نفذ التحميل الكسول لجميع صور المنتجات
6. استخرج CSS الحرج وضمنه
7. شغّل Lighthouse مرة أخرى وقارن النتائج
8. وثق تحسينات الأداء

ملخص نتائج الجزء الأول

بعد تنفيذ جميع تحسينات الواجهة الأمامية:

<!-- مقاييس قبل وبعد -->

قبل التحسين:
- أداء Lighthouse: 42/100
- First Contentful Paint: 3.2 ثانية
- Largest Contentful Paint: 7.1 ثانية
- Time to Interactive: 8.5 ثانية
- Total Blocking Time: 2,140ms
- وزن الصفحة: 4.2 ميجابايت
- طلبات HTTP: 127

بعد تحسين الواجهة الأمامية:
- أداء Lighthouse: 78/100 ✓
- First Contentful Paint: 1.1 ثانية ✓ (تحسين 66%)
- Largest Contentful Paint: 2.1 ثانية ✓ (تحسين 70%)
- Time to Interactive: 3.1 ثانية ✓ (تحسين 64%)
- Total Blocking Time: 420ms ✓ (تحسين 80%)
- وزن الصفحة: 850 كيلوبايت ✓ (تخفيض 80%)
- طلبات HTTP: 38 ✓ (تخفيض 70%)

المشاكل المتبقية (الجزء 2):
- وقت استجابة الخادم البطيء (1.2 ثانية)
- عدم كفاءة استعلامات قاعدة البيانات
- بدون تخزين مؤقت من جانب الخادم
- نقاط نهاية API غير محسّنة

تقدم ممتاز! لقد حققنا تحسينات كبيرة في الواجهة الأمامية. في الجزء الثاني، سنتعامل مع تحسينات جانب الخادم وقاعدة البيانات للوصول إلى هدفنا المتمثل في أوقات تحميل أقل من ثانيتين ودرجة Lighthouse أعلى من 90.