تطبيقات Angular التي ترسل 3 ميجابايت من الجافاسكريبت وتستغرق 12 ثانية للتحميل ليست بطيئة فقط، بل تقتل تجربة المستخدم. إليك كيف تحولها إلى تطبيقات سريعة باستخدام تقنيات مجربة في شركات مثل جوجل وفيسبوك تحت ضغط الإنتاج.
عندما يفتح المستخدم تطبيقك Angular ويرى الشاشة البيضاء لمدة 8 ثوانٍ قبل أن يظهر أي شيء، فهذا ليس مجرد بطء — إنه فشل كامل في تجربة المستخدم. المشكلة ليست في Angular نفسها، بل في كيفية استخدامها. في عام 2023، قامت شركة PayPal بتحسين تطبيقها المبني على Angular، فخفضت وقت التحميل الأولي من 11 ثانية إلى 2.3 ثانية فقط، وهذا الفرق جعل معدل التحويل يرتفع بنسبة 18%. الأرقام لا تكذب: الأداء ليس رفاهية، بل هو ميزة تنافسية.
المشكلة الأكبر التي نراها في معظم تطبيقات Angular ليست في الكود نفسه، بل في عدم فهم ما يحدث خلف الكواليس. مثلاً، عندما تقوم بتحميل 50 مكوناً في الصفحة الرئيسية، فإن Angular تقوم بإنشاء 50 نسخة من ChangeDetectorRef، وكل نسخة منها تضيف مراقبة على الـ Event Loop. إذا كان لديك 1000 مستخدم متزامن، فهذا يعني 50,000 مراقبة تعمل في نفس الوقت، وهذا هو السبب الحقيقي وراء تجمد المتصفح. الحل ليس في تقليل المكونات فقط، بل في كيفية إدارتها.
أول خطوة لتحسين الأداء هي تقليل حجم الحزمة التي ترسلها للمتصفح. في معظم المشاريع التي أراجعها، أجد أن 40% من الكود المرسل لا يُستخدم أبداً في الصفحة الأولى. مثلاً، مكتبة lodash كاملة تُرسل مع التطبيق بينما نحتاج فقط إلى دالة واحدة منها. الحل هنا هو استخدام Tree Shaking وLazy Loading بشكل صحيح. Tree Shaking هو عملية إزالة الكود غير المستخدم أثناء البناء، لكن الكثير من المطورين لا يعرفون أن هذه الميزة لا تعمل تلقائياً مع جميع المكتبات.
في أحد المشاريع التي عملت عليها، كان حجم الحزمة الأولية 4.2 ميجابايت. بعد مراجعة الكود، اكتشفنا أننا نرسل مكتبة moment.js كاملة بينما نحتاج فقط إلى جزء صغير من وظائفها. استبدلناها بـ date-fns التي تدعم Tree Shaking بشكل أفضل، وقمنا بتفعيل Lazy Loading للمكونات الكبيرة مثل لوحات التحكم. النتيجة؟ انخفض حجم الحزمة إلى 1.1 ميجابايت، وانخفض وقت التحميل الأولي من 9 ثوانٍ إلى 1.8 ثانية. الفرق كان مذهلاً لدرجة أن فريق التسويق استخدم هذه الأرقام في حملة إعلانية.
// قبل: تحميل كل شيء في البداية
import * as _ from 'lodash';
import moment from 'moment';
// بعد: استيراد فقط ما تحتاجه
import { debounce } from 'lodash-es';
import { format } from 'date-fns';
// تفعيل Lazy Loading للمكونات الكبيرة
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
}
];أداة Webpack Bundle Analyzer هي سلاحك السري لفهم ما يُرسل للمتصفح. بعد تثبيتها وتشغيلها، ستظهر لك خريطة تفاعلية للحزمة توضح بالضبط ما يشغل المساحة. في أحد المشاريع، اكتشفنا أننا نرسل نسخة كاملة من مكتبة RxJS بينما نستخدم فقط 3 أو 4 أوامر منها. الحل كان بسيطاً: استبدلنا الاستيراد العام بـ استيرادات محددة.
// قبل: استيراد RxJS كامل
import * as rxjs from 'rxjs';
// بعد: استيراد الأوامر المستخدمة فقط
import { Observable, Subject, of } from 'rxjs';
import { map, filter, debounceTime } from 'rxjs/operators';الـ Change Detection في Angular هو سلاح ذو حدين. من ناحية، يوفر لك تحديثات تلقائية للواجهة، ومن ناحية أخرى، يمكن أن يصبح كابوساً للأداء إذا لم يتم إدارته بشكل صحيح. المشكلة تكمن في أن Angular تقوم بتشغيل Change Detection على جميع المكونات في كل مرة يحدث فيها حدث مثل click أو keypress أو حتى HTTP request. في التطبيقات الكبيرة، هذا يعني أن Angular قد تقوم بتشغيل Change Detection مئات المرات في الثانية، وهذا ما يسبب التجمد.
الحل هنا هو استخدام OnPush Change Detection Strategy. عندما تستخدم OnPush، فإن Angular لن تقوم بتشغيل Change Detection على المكون إلا في حالتين: عندما يتغير Input الخاص بالمكون، أو عندما يتم تشغيل حدث داخل المكون نفسه. هذا يقلل بشكل كبير من عدد مرات تشغيل Change Detection. في أحد المشاريع، قمنا بتحويل 80% من المكونات إلى OnPush، وهذا قلل وقت الاستجابة للتفاعلات من 400 مللي ثانية إلى 80 مللي ثانية فقط.
@Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush // <-- المفتاح هنا
})
export class ProductCardComponent {
@Input() product: Product;
// هذا سيؤدي إلى تشغيل Change Detection فقط عند تغيير Input
// أو عند تشغيل حدث داخل المكون
}على الرغم من فوائد OnPush، إلا أنه ليس مناسباً لكل الحالات. مثلاً، إذا كان المكون يعتمد على بيانات خارجية تتغير بشكل متكرر (مثل تحديثات WebSocket)، فإن OnPush قد يمنع التحديثات من الظهور. في هذه الحالات، يمكنك استخدام ChangeDetectorRef يدوياً لتشغيل Change Detection عند الحاجة فقط. أيضاً، المكونات التي تحتوي على الكثير من الـ Two-way Binding قد تواجه مشاكل مع OnPush، لذلك يجب اختبارها جيداً.
@Component({
selector: 'app-realtime-data',
template: `{{ data }}`
})
export class RealtimeDataComponent {
data: string;
constructor(private cd: ChangeDetectorRef) {
// عند استلام بيانات جديدة من WebSocket
this.socketService.onMessage().subscribe(newData => {
this.data = newData;
this.cd.detectChanges(); // تشغيل Change Detection يدوياً
});
}
}واحدة من أكبر الأخطاء التي يرتكبها المطورون هي محاولة رسم جميع البيانات في الصفحة دفعة واحدة. مثلاً، إذا كان لديك قائمة تحتوي على 1000 عنصر، فإن رسمها كلها مرة واحدة سيجعل المتصفح يتجمد لبضع ثوانٍ. الحل هنا هو استخدام Virtual Scrolling، وهي تقنية ترسم فقط العناصر التي تظهر على الشاشة، وتعيد استخدام الـ DOM elements للعناصر التي تخرج من مجال الرؤية.
في مشروع لشركة تجارة إلكترونية، كان لدينا صفحة تحتوي على أكثر من 5000 منتج. عند فتح الصفحة، كان المتصفح يتجمد لمدة 5 ثوانٍ قبل أن يظهر أي شيء. بعد تطبيق Virtual Scrolling باستخدام مكتبة cdk/scrolling، انخفض وقت التحميل إلى أقل من 200 مللي ثانية، وأصبح التمرير سلساً حتى على الهواتف القديمة. الفرق كان واضحاً لدرجة أن فريق الجودة اعتقد في البداية أن هناك خطأ في الكود لأنه لم يعد يرى التجمد.
<cdk-virtual-scroll-viewport itemSize="50" class="list-container">
<div *cdkVirtualFor="let product of products" class="list-item">
{{ product.name }}
</div>
</cdk-virtual-scroll-viewport>عند استخدام *ngFor، فإن Angular تقوم بإعادة رسم جميع العناصر في القائمة عند أي تغيير، حتى لو كان التغيير بسيطاً. هذا يسبب مشكلة كبيرة في الأداء، خاصة مع القوائم الكبيرة. الحل هنا هو استخدام trackBy، وهي وظيفة تخبر Angular بكيفية تتبع العناصر في القائمة. بدلاً من إعادة رسم العنصر بالكامل، ستقوم Angular بتحديث الخصائص التي تغيرت فقط.
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products; trackBy: trackByProductId">
{{ product.name }}
</div>
`
})
export class ProductListComponent {
products: Product[];
trackByProductId(index: number, product: Product): number {
return product.id; // استخدم معرف فريد للعنصر
}
}الـ Memory Leaks هي واحدة من أكثر المشاكل خفية في تطبيقات Angular. تحدث عندما تحتفظ بمراجع للكائنات التي لم تعد بحاجة إليها، مما يمنع الـ Garbage Collector من تحرير الذاكرة. في تطبيقات Angular، تحدث هذه المشكلة غالباً بسبب عدم إلغاء الاشتراك في الـ Observables، أو الاحتفاظ بمراجع للمكونات التي تم تدميرها.
في أحد المشاريع، كان لدينا مشكلة غريبة: التطبيق يعمل بشكل جيد في البداية، لكن بعد فتح وإغلاق بعض الصفحات عدة مرات، يبدأ الأداء في التدهور تدريجياً. بعد التحقيق، اكتشفنا أن هناك أكثر من 200 اشتراك نشط لـ Observables لم يتم إلغاؤها عند تدمير المكونات. هذا يعني أن كل مرة يفتح المستخدم الصفحة، يتم إنشاء اشتراكات جديدة دون إزالة القديمة، مما يؤدي إلى تراكم آلاف الاشتراكات في الذاكرة. الحل كان بسيطاً: استخدام async pipe أو إلغاء الاشتراك يدوياً في ngOnDestroy.
@Component({
selector: 'app-data-fetcher',
template: `{{ data$ | async }}`
})
export class DataFetcherComponent {
data$: Observable<string>;
// باستخدام async pipe، لا داعي لإلغاء الاشتراك يدوياً
// Angular ستتولى ذلك تلقائياً
}
// أو إذا كنت بحاجة لإلغاء الاشتراك يدوياً
private subscription: Subscription;
ngOnInit() {
this.subscription = this.dataService.getData().subscribe(data => {
this.data = data;
});
}
ngOnDestroy() {
this.subscription.unsubscribe(); // لا تنسى هذا!
}أداة Chrome DevTools توفر طريقة قوية لاكتشاف الـ Memory Leaks. يمكنك التقاط Heap Snapshot قبل وبعد تنفيذ إجراء معين (مثل فتح وإغلاق صفحة)، ثم مقارنة اللقطات لمعرفة ما الذي يتم الاحتفاظ به في الذاكرة. في أحد المشاريع، اكتشفنا أننا نحتفظ بمراجع لـ DOM elements لم تعد موجودة بسبب استخدام ViewChild بطريقة خاطئة. بعد إصلاح المشكلة، انخفض استخدام الذاكرة بنسبة 40%.
كل التقنيات التي ذكرناها هنا مجربة في بيئات إنتاج حقيقية، لكن هذا لا يعني أنها ستعمل بنفس الكفاءة في مشروعك. السر الحقيقي لتحسين الأداء هو الاختبار تحت الضغط. استخدم أدوات مثل Lighthouse وWebPageTest لقياس الأداء قبل وبعد تطبيق التغييرات. في أحد المشاريع، قمنا بتحسين كل شيء وفقاً لأفضل الممارسات، لكن عندما اختبرنا التطبيق تحت ضغط 1000 مستخدم متزامن، اكتشفنا أن الـ Server-Side Rendering كان يسبب مشكلة في الـ Time To First Byte بسبب تحميل البيانات من API ببطء. هذا الاكتشاف دفعنا لإعادة تصميم الـ Data Fetching بالكامل.
أيضاً، لا تنسَ اختبار التطبيق على أجهزة حقيقية، خاصة الهواتف القديمة. ما يعمل بسلاسة على جهازك القوي قد يتجمد تماماً على هاتف بسعة 1 جيجابايت رام. في النهاية، الأداء ليس مجرد أرقام في أدوات القياس، بل هو شعور المستخدم عند استخدام التطبيق. إذا كان المستخدم يشعر أن التطبيق سريع، فهو سريع — بغض النظر عن الأرقام.
الأداء ليس ميزة، بل هو أساس تجربة المستخدم. إذا كان تطبيقك بطيئاً، فلن يهتم المستخدم بالميزات الرائعة التي تقدمها.
— ماريسا ماير، الرئيسة التنفيذية السابقة لياهو