تطبيقات الويب التقدمية

تحسين الأداء لتطبيقات PWA

20 دقيقة الدرس 14 من 30

مقدمة إلى أداء PWA

الأداء أمر بالغ الأهمية لتطبيقات الويب التقدمية. يتوقع المستخدمون تجارب سريعة ومستجيبة، خاصة على الأجهزة المحمولة. الأداء الضعيف يؤدي إلى معدلات ارتداد أعلى وانخفاض تفاعل المستخدم.

إحصائيات الأداء:
  • 53% من مستخدمي الهاتف المحمول يتخلون عن المواقع التي تستغرق أكثر من 3 ثوانٍ للتحميل
  • تأخير ثانية واحدة في تحميل الصفحة يمكن أن يؤدي إلى انخفاض بنسبة 7% في التحويلات
  • تستخدم Google مقاييس الأداء كعوامل تصنيف في نتائج البحث

تدقيقات Lighthouse

Lighthouse هي أداة آلية لقياس أداء PWA وإمكانية الوصول وSEO وأفضل الممارسات:

تشغيل Lighthouse

# تثبيت Lighthouse CLI npm install -g lighthouse # تشغيل التدقيق lighthouse https://your-pwa.com --view # تشغيل التدقيق مع فئات محددة lighthouse https://your-pwa.com --only-categories=performance,pwa --view # إنشاء تقرير JSON lighthouse https://your-pwa.com --output=json --output-path=./report.json # التشغيل في وضع بدون واجهة lighthouse https://your-pwa.com --chrome-flags="--headless"

Lighthouse في Chrome DevTools

  1. افتح Chrome DevTools (F12)
  2. اذهب إلى تبويب "Lighthouse"
  3. اختر الفئات للتدقيق (الأداء، PWA، إلخ)
  4. اختر نوع الجهاز (الهاتف المحمول أو سطح المكتب)
  5. انقر على "Generate report"
نصيحة: اختبر دائماً على الهاتف المحمول مع تفعيل الخنق. أداء سطح المكتب لا يعكس تجربة الهاتف المحمول الواقعية. استخدم خنق "Slow 4G" للحصول على نتائج واقعية.

المؤشرات الحيوية الأساسية للويب

المؤشرات الحيوية الأساسية للويب هي مقاييس Google لقياس تجربة المستخدم في العالم الحقيقي:

1. أكبر رسم محتوى (LCP)

يقيس أداء التحميل. يجب أن يحدث LCP خلال 2.5 ثانية من تحميل الصفحة.

// قياس LCP new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime); // إرسال إلى التحليلات sendToAnalytics({ metric: 'LCP', value: lastEntry.renderTime || lastEntry.loadTime, rating: lastEntry.renderTime < 2500 ? 'good' : 'needs-improvement' }); }).observe({ entryTypes: ['largest-contentful-paint'] });

تحسين LCP:

  • تحسين وضغط الصور
  • التحميل المسبق للموارد الحرجة
  • إزالة JavaScript و CSS المانعة للعرض
  • استخدام العرض من جانب الخادم أو التوليد الثابت
  • تنفيذ استراتيجيات تخزين مؤقت فعالة

2. تأخير الإدخال الأول (FID) / التفاعل إلى الرسم التالي (INP)

يقيس التفاعلية. يجب أن يكون FID أقل من 100ms، INP أقل من 200ms.

// قياس FID new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { const delay = entry.processingStart - entry.startTime; console.log('FID:', delay); sendToAnalytics({ metric: 'FID', value: delay, rating: delay < 100 ? 'good' : 'needs-improvement' }); }); }).observe({ entryTypes: ['first-input'] });

تحسين FID/INP:

  • تقسيم مهام JavaScript الطويلة
  • استخدام Web Workers للحسابات الثقيلة
  • تحسين معالجات الأحداث
  • تأجيل JavaScript غير الحرج
  • استخدام تقسيم الكود لتقليل حجم الحزمة

3. تحول التخطيط التراكمي (CLS)

يقيس الاستقرار البصري. يجب أن يكون CLS أقل من 0.1.

// قياس CLS let clsValue = 0; let sessionValue = 0; let sessionEntries = []; new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) { sessionValue += entry.value; sessionEntries.push(entry); if (sessionValue > clsValue) { clsValue = sessionValue; console.log('CLS:', clsValue); sendToAnalytics({ metric: 'CLS', value: clsValue, rating: clsValue < 0.1 ? 'good' : 'needs-improvement' }); } } }); }).observe({ entryTypes: ['layout-shift'] });

تحسين CLS:

  • قم دائماً بتضمين سمات الحجم على الصور ومقاطع الفيديو
  • احجز مساحة للإعلانات والمضمنات
  • تجنب إدراج محتوى فوق المحتوى الموجود
  • استخدام CSS aspect-ratio للوسائط
  • التحميل المسبق للخطوط لمنع FOIT/FOUT

التحميل الكسول

تأجيل تحميل الموارد غير الحرجة حتى الحاجة إليها:

التحميل الكسول الأصلي للصور

<!-- المتصفح يحمل الصور تحت الطية بشكل كسول تلقائياً --> <img src="image.jpg" alt="الوصف" loading="lazy"> <!-- التحميل الفوري (للصور فوق الطية) --> <img src="hero.jpg" alt="البطل" loading="eager"> <!-- يعمل أيضاً مع إطارات iframe --> <iframe src="video.html" loading="lazy"></iframe>

التحميل الكسول مع Intersection Observer

// التحميل الكسول للصور مع احتياطي للمتصفحات القديمة const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.add('loaded'); observer.unobserve(img); } }); }, { rootMargin: '50px' // بدء التحميل 50px قبل الدخول إلى viewport }); // مراقبة جميع الصور مع سمة data-src document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); });
<!-- ترميز HTML --> <img data-src="large-image.jpg" src="placeholder.jpg" alt="الوصف" class="lazy">

التحميل الكسول لوحدات JavaScript

// الاستيراد الديناميكي - يحمل الوحدة فقط عند الحاجة async function loadFeature() { const module = await import('./feature.js'); module.initialize(); } // التحميل عند النقر على الزر document.getElementById('load-btn').addEventListener('click', loadFeature); // التحميل عندما يدخل العنصر viewport const featureObserver = new IntersectionObserver(async (entries) => { if (entries[0].isIntersecting) { const { default: initFeature } = await import('./feature.js'); initFeature(); featureObserver.disconnect(); } }); featureObserver.observe(document.getElementById('feature-section'));

تقسيم الكود

قسم حزمة JavaScript الخاصة بك إلى أجزاء أصغر لتقليل وقت التحميل الأولي:

تقسيم الكود على أساس المسار

// استخدام React (مثال) import { lazy, Suspense } from 'react'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); const Contact = lazy(() => import('./pages/Contact')); function App() { return ( <Suspense fallback={<div>جاري التحميل...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> </Routes> </Suspense> ); }

تقسيم كود Webpack

// webpack.config.js module.exports = { entry: './src/index.js', output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js' }, optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 }, common: { minChunks: 2, priority: 5, reuseExistingChunk: true } } } } };

تحسين الصور

صيغ الصور الحديثة

<picture> <!-- WebP للمتصفحات التي تدعمه --> <source srcset="image.webp" type="image/webp"> <!-- AVIF لأفضل ضغط --> <source srcset="image.avif" type="image/avif"> <!-- احتياطي إلى JPEG --> <img src="image.jpg" alt="الوصف" loading="lazy"> </picture>

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

# تثبيت imagemin npm install imagemin imagemin-mozjpeg imagemin-pngquant imagemin-svgo imagemin-webp # optimize-images.js const imagemin = require('imagemin'); const imageminMozjpeg = require('imagemin-mozjpeg'); const imageminPngquant = require('imagemin-pngquant'); const imageminWebp = require('imagemin-webp'); (async () => { await imagemin(['images/*.{jpg,png}'], { destination: 'images/optimized', plugins: [ imageminMozjpeg({ quality: 80 }), imageminPngquant({ quality: [0.6, 0.8] }), imageminWebp({ quality: 80 }) ] }); })();

الصور المتجاوبة

<img src="small.jpg" srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w" sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw" alt="صورة متجاوبة" loading="lazy">

تحسين الخطوط

استراتيجيات تحميل الخطوط

/* 1. عرض الخط */ @font-face { font-family: 'CustomFont'; src: url('font.woff2') format('woff2'); font-display: swap; /* إظهار احتياطي فوراً، التبديل عند التحميل */ /* خيارات أخرى: auto, block, fallback, optional */ } /* 2. التحميل المسبق للخطوط الحرجة */ <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> /* 3. استخدام خطوط النظام للعرض الفوري */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; }

تقسيم الخطوط الفرعية

# تثبيت glyphhanger npm install -g glyphhanger # إنشاء مجموعة فرعية مع الأحرف المستخدمة فقط glyphhanger --subset=font.ttf --formats=woff2 # مجموعة فرعية لنص محدد glyphhanger --subset=font.ttf --whitelist="ABCDEFGabcdefg0123456789"

CSS الحرجة

أضف CSS الحرجة مضمنة وأجل الأنماط غير الحرجة:

<!DOCTYPE html> <html> <head> <!-- CSS حرجة مضمنة --> <style> /* أنماط فوق الطية فقط */ body { margin: 0; font-family: sans-serif; } .header { background: #333; color: white; padding: 1rem; } .hero { min-height: 100vh; } </style> <!-- التحميل المسبق لـ CSS الكاملة --> <link rel="preload" href="styles.css" as="style"> <!-- تحميل CSS الكاملة بشكل غير متزامن --> <link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'"> <!-- احتياطي للمتصفحات بدون JS --> <noscript> <link rel="stylesheet" href="styles.css"> </noscript> </head> <body> <!-- المحتوى --> </body> </html>

إنشاء CSS الحرجة تلقائياً

# تثبيت critical npm install critical # generate-critical.js const critical = require('critical'); critical.generate({ inline: true, base: 'dist/', src: 'index.html', target: 'index.html', width: 1300, height: 900, minify: true });

تلميحات الموارد

DNS Prefetch

<!-- حل DNS مبكراً للنطاقات الخارجية --> <link rel="dns-prefetch" href="//fonts.googleapis.com"> <link rel="dns-prefetch" href="//api.example.com">

Preconnect

<!-- إنشاء اتصال مبكر بالأصول الحرجة --> <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin> <link rel="preconnect" href="https://cdn.example.com">

Prefetch

<!-- جلب الموارد المطلوبة على الأرجح للتنقل التالي --> <link rel="prefetch" href="/about.html"> <link rel="prefetch" href="/styles/about.css">

Preload

<!-- موارد عالية الأولوية مطلوبة للصفحة الحالية --> <link rel="preload" href="hero.jpg" as="image"> <link rel="preload" href="critical.css" as="style"> <link rel="preload" href="app.js" as="script"> <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

أداء Service Worker

// أنماط Service Worker الفعالة self.addEventListener('fetch', (event) => { // تخطي طلبات غير GET if (event.request.method !== 'GET') return; // تخطي امتدادات Chrome والمخططات الأخرى if (!event.request.url.startsWith('http')) return; // استخدام استراتيجيات مختلفة بناءً على نوع الطلب if (event.request.destination === 'image') { event.respondWith(cacheFirst(event.request)); } else if (event.request.destination === 'document') { event.respondWith(networkFirst(event.request)); } }); // استراتيجية cache-first فعالة async function cacheFirst(request) { const cache = await caches.open('v1'); const cached = await cache.match(request); return cached || fetch(request); }
تمرين:
  1. قم بتشغيل تدقيق Lighthouse على PWA الخاص بك وحدد مشاكل الأداء
  2. قس المؤشرات الحيوية الأساسية للويب (LCP، FID/INP، CLS) في تطبيقك
  3. نفذ التحميل الكسول لجميع الصور تحت الطية
  4. حسّن 5 صور على الأقل باستخدام الصيغ الحديثة (WebP/AVIF)
  5. أضف CSS الحرجة المضمنة إلى HTML الخاص بك
  6. نفذ تقسيم الكود لمسارين على الأقل
  7. أضف تلميحات الموارد المناسبة (preconnect، prefetch، preload)
  8. أعد تشغيل Lighthouse واستهدف درجة أعلى من 90
تحذير: لا تفرط في التحسين. ركز على المقاييس الأكثر أهمية لمستخدميك. التحسين المبكر يمكن أن يؤدي إلى كود معقد يصعب صيانته. قس دائماً قبل وبعد التحسين لضمان التحسينات.