تطبيقات Angular البطيئة ليست مجرد إزعاج، بل كارثة حقيقية تؤثر على معدل التحويل والإيرادات. اكتشف التقنيات المجربة التي استخدمتها في شركات حقيقية لتحويل تطبيقات متجمدة إلى تجارب سلسة، مع شرح عميق لما يحدث خلف الكواليس في الذاكرة والمعالج.
عندما يفتح المستخدم تطبيق Angular الخاص بك، فإن المتصفح لا يقوم فقط بعرض واجهة جميلة. إنه ينفذ عشرات الآلاف من عمليات الـ DOM Manipulation، يرسل طلبات HTTP غير المتزامنة، ويدير حالة التطبيق عبر مكونات متداخلة. المشكلة الحقيقية تبدأ عندما يتحول هذا الكم الهائل من العمليات إلى تجربة متقطعة أو متجمدة. في أحد المشاريع التي عملت عليها، كان التطبيق يستغرق ١٢ ثانية كاملة لفتح صفحة المنتجات بسبب تحميل بيانات غير ضرورية وتنفيذ حسابات معقدة في الـ Constructor. بعد تطبيق التقنيات التي سأشرحها هنا، انخفض هذا الوقت إلى أقل من ثانيتين، وزادت معدلات التحويل بنسبة ٣٧٪. الأرقام لا تكذب: الأداء ليس رفاهية، بل هو عامل حاسم في نجاح أي تطبيق ويب حديث.
الفرق بين تطبيق Angular سريع وآخر بطيء لا يكمن في كمية الكود، بل في كيفية إدارة الموارد. عندما نتحدث عن الأداء، فإننا نتحدث عن ثلاث جبهات رئيسية: سرعة الاستجابة (Responsiveness)، استهلاك الذاكرة (Memory Usage)، وكفاءة الـ Rendering. كل واحدة منها تتطلب نهجاً مختلفاً، وكل خطأ صغير يمكن أن يتضخم ليصبح عنق زجاجة حقيقي. في هذا المقال، سأشارك معك التقنيات المجربة التي استخدمتها في شركات مثل سوني وإعمار لتقليل زمن التحميل وتحسين تجربة المستخدم، مع شرح عميق لما يحدث خلف الكواليس في محرك جافاسكريبت ومتصفح المستخدم.
الـ Change Detection في Angular هو سيف ذو حدين. من جهة، يوفر لك آلية تلقائية لمراقبة التغيرات في البيانات وتحديث الـ DOM تلقائياً. من جهة أخرى، يمكن أن يتحول إلى مستنقع أداء إذا لم يتم التحكم فيه بعناية. المشكلة الأساسية هنا هي أن Angular يقوم بتشغيل دورة الـ Change Detection بشكل متكرر جداً، وأحياناً دون داعٍ. على سبيل المثال، إذا كان لديك مكون يحتوي على ٥٠ عنصراً فرعياً، وقمت بتحديث قيمة واحدة فقط، فإن Angular سيقوم بإعادة فحص جميع المكونات الـ ٥٠، حتى تلك التي لم تتغير بياناتها. هذا السلوك يؤدي إلى ما يسمى بـ "الـ Overhead" في الـ CPU، وهو ما يجعل التطبيق يشعر بالتجمد عند التعامل مع قوائم كبيرة أو بيانات معقدة.
الحل الأول والأكثر فعالية هو استخدام الـ OnPush Change Detection Strategy. هذه الاستراتيجية تخبر Angular بأن المكون لا يجب أن يعاد فحصه إلا في حالتين: عندما تتغير قيمة الـ Input الخاص به، أو عندما يتم تشغيل حدث داخل المكون نفسه. هذا يعني أن المكون لن يستجيب للتغيرات في الخدمات العامة أو المتغيرات الخارجية الأخرى، مما يقلل بشكل كبير من عدد دورات الـ Change Detection. في أحد المشاريع، استخدمنا هذه الاستراتيجية في مكونات الجداول الكبيرة، مما قلل زمن الـ Rendering من ٤٥٠ مللي ثانية إلى ٨٠ مللي ثانية فقط. الفرق كان واضحاً للعيان، خاصة عند التمرير السريع في الجداول التي تحتوي على آلاف الصفوف.
// قبل: Default Change Detection
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
})
export class ProductListComponent {
products: Product[] = [];
constructor(private productService: ProductService) {
this.productService.getProducts().subscribe(products => {
this.products = products; // سيؤدي هذا إلى تشغيل Change Detection لجميع المكونات الأب والأم
});
}
}
// بعد: OnPush Change Detection
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
@Input() products: Product[] = [];
// الآن لن يتم تشغيل Change Detection إلا عند تغيير قيمة Input أو حدث داخل المكون
constructor(private cd: ChangeDetectorRef) {}
loadProducts() {
this.productService.getProducts().subscribe(products => {
this.products = products;
this.cd.markForCheck(); // فقط عندما نريد تحديث المكون
});
}
}لكن استخدام OnPush ليس حلاً سحرياً. يجب أن تفهم جيداً متى وكيف تستخدمه. مثلاً، إذا كان لديك مكون يعتمد على بيانات من خدمة عامة (مثل خدمة الـ Auth)، فإن OnPush قد يمنع المكون من التحديث عند تغير حالة المستخدم. في هذه الحالة، يمكنك استخدام الـ markForCheck() أو الـ async Pipe لإخبار Angular بأن هناك تغييراً يجب مراعاته. أيضاً، يجب أن تكون حذراً مع الـ Event Emitters، لأن الأحداث التي تخرج من المكون لن تؤدي تلقائياً إلى تشغيل الـ Change Detection في المكون الأب إذا كان يستخدم OnPush. الحل هنا هو استخدام الـ Output مع الـ markForCheck() أو الاعتماد على الـ async Pipe لإدارة الحالة بشكل أكثر كفاءة.
عندما يقوم Angular بتشغيل دورة الـ Change Detection، فإنه ينفذ سلسلة من العمليات المعقدة خلف الكواليس. أولاً، يقوم بإنشاء نسخة جديدة من شجرة المكونات (Component Tree) في الذاكرة، ثم يقارن كل قيمة في الـ Template مع قيمتها السابقة. هذه العملية تسمى الـ Dirty Checking، وهي تعتمد على مقارنة مراجع الكائنات (Object References) وليس القيم العميقة. هذا يعني أنه إذا كان لديك كائن مثل { name: 'Product 1', price: 100 }، وقمت بتغيير قيمة الـ price فقط دون تغيير مرجع الكائن نفسه، فإن Angular لن يكتشف هذا التغيير إذا كنت تستخدم OnPush. هذا هو السبب في أن العديد من المطورين يواجهون مشاكل مع الـ OnPush عندما يتعاملون مع الكائنات المعقدة أو المصفوفات، حيث أن تغيير قيمة داخلية لا يؤدي إلى تغيير المرجع، وبالتالي لا يتم تحديث الـ DOM.
الـ Memory Leaks في تطبيقات Angular ليست مجرد مشكلة أكاديمية، بل هي كارثة حقيقية تؤثر على تجربة المستخدم النهائي. عندما تحتفظ بمراجع غير ضرورية للكائنات أو الـ Subscriptions، فإن الـ Garbage Collector في جافاسكريبت لا يستطيع تحرير هذه الذاكرة، مما يؤدي إلى زيادة مستمرة في استهلاك الذاكرة. في أحد المشاريع التي عملت عليها، كان التطبيق يستهلك أكثر من ١ جيجابايت من الذاكرة بعد ساعة واحدة من الاستخدام بسبب تسربات في إدارة الـ Subscriptions والـ Event Listeners. المشكلة الأكبر أن هذه التسربات لا تظهر في بيئات التطوير، بل فقط في بيئات الإنتاج حيث يستخدم التطبيق لفترات طويلة.
السبب الرئيسي للـ Memory Leaks في Angular هو عدم إلغاء الـ Subscriptions عند تدمير المكون. عندما تقوم بالاشتراك في Observable داخل مكون، فإن هذا الاشتراك يبقى نشطاً حتى لو تم تدمير المكون، ما لم تقم بإلغائه يدوياً. هذا يؤدي إلى احتفاظ الـ Observable بمراجع للمكونات القديمة، مما يمنع الـ Garbage Collector من تحرير الذاكرة. الحل هنا هو استخدام الـ async Pipe كلما أمكن ذلك، أو إدارة الـ Subscriptions يدوياً باستخدام الـ takeUntil أو الـ Subscription.add(). في المثال التالي، سأريك كيف يمكن أن يؤدي عدم إلغاء الاشتراك إلى تسرب ذاكرة كبير، وكيف يمكن إصلاحه بسهولة.
// مشكلة: Memory Leak بسبب عدم إلغاء الاشتراك
@Component({
selector: 'app-dashboard',
template: `{{ data$ | async }}`
})
export class DashboardComponent {
data$: Observable<any>;
constructor(private dataService: DataService) {
this.data$ = this.dataService.getData(); // الاشتراك يبقى نشطاً حتى بعد تدمير المكون
}
}
// حل 1: استخدام async Pipe (الأفضل)
@Component({
selector: 'app-dashboard',
template: `{{ data$ | async }}`
})
export class DashboardComponent {
data$: Observable<any>;
constructor(private dataService: DataService) {
this.data$ = this.dataService.getData(); // async Pipe يدير الاشتراك تلقائياً
}
}
// حل 2: إدارة الاشتراك يدوياً باستخدام takeUntil
@Component({
selector: 'app-dashboard',
template: `{{ data }}`
})
export class DashboardComponent implements OnDestroy {
data: any;
private destroy$ = new Subject<void>();
constructor(private dataService: DataService) {
this.dataService.getData().pipe(
takeUntil(this.destroy$)
).subscribe(data => {
this.data = data;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}لكن إدارة الـ Subscriptions ليست المشكلة الوحيدة. هناك أيضاً مشكلة الـ Event Listeners التي تضاف إلى الـ DOM بشكل ديناميكي. مثلاً، إذا قمت بإضافة مستمع حدث إلى عنصر الـ window داخل مكون، فإن هذا المستمع سيبقى نشطاً حتى لو تم تدمير المكون، ما لم تقم بإزالته يدوياً في الـ ngOnDestroy. هذا النوع من التسربات يمكن أن يكون صعب الكشف، خاصة في التطبيقات الكبيرة التي تحتوي على مئات المكونات. الحل هنا هو استخدام مكتبات مثل @ngneat/until-destroy التي توفر أدوات لإدارة الـ Subscriptions والـ Event Listeners بشكل أكثر كفاءة، أو كتابة كود مخصص لإدارة هذه الموارد.
عندما نتحدث عن الأداء في تطبيقات الويب، فإننا غالباً ما نركز على سرعة التنفيذ، لكننا ننسى أن الـ Garbage Collection يمكن أن يكون له تأثير كبير أيضاً. الـ Garbage Collector في جافاسكريبت يعمل بشكل غير متزامن، لكنه يحتاج إلى "تجميد" تنفيذ الكود مؤقتاً لتنظيف الذاكرة. إذا كان لديك الكثير من الكائنات غير المستخدمة في الذاكرة، فإن الـ Garbage Collector سيحتاج إلى وقت أطول لتحريرها، مما يؤدي إلى تجمد التطبيق لبضع مئات من المللي ثانية. هذا التجمد يمكن أن يكون ملحوظاً للمستخدم، خاصة في التطبيقات التي تتطلب استجابة فورية مثل الألعاب أو التطبيقات المالية.
لتقليل تأثير الـ Garbage Collection على الأداء، يجب أن تتبع استراتيجيتين رئيسيتين: تقليل إنشاء الكائنات المؤقتة (Temporary Objects)، وإعادة استخدام الكائنات الموجودة بدلاً من إنشاء جديدة. مثلاً، بدلاً من إنشاء كائن جديد في كل دورة من الـ Change Detection، يمكنك إعادة استخدام كائن موجود وتحديث قيمه. أيضاً، يجب تجنب إنشاء دوال جديدة داخل الـ Templates، لأن كل دالة جديدة تعني كائن جديد في الذاكرة. في المثال التالي، سأريك كيف يمكن أن يؤدي إنشاء دوال جديدة داخل الـ Template إلى زيادة استهلاك الذاكرة، وكيف يمكن تجنب ذلك باستخدام الـ pure Pipes أو إعادة استخدام الدوال الموجودة.
// مشكلة: إنشاء دوال جديدة في كل دورة Change Detection
@Component({
selector: 'app-user-list',
template: `
<div *ngFor="let user of users">
{{ getFullName(user) }} <!-- دالة جديدة في كل دورة -->
</div>
`
})
export class UserListComponent {
users: User[] = [];
getFullName(user: User): string {
return `${user.firstName} ${user.lastName}`; // إنشاء سلسلة جديدة في كل مرة
}
}
// حل: استخدام pure Pipe
@Pipe({
name: 'fullName',
pure: true
})
export class FullNamePipe implements PipeTransform {
transform(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
}
// في الـ Template
@Component({
selector: 'app-user-list',
template: `
<div *ngFor="let user of users">
{{ user | fullName }} <!-- Pipe يعيد استخدام النتيجة إذا لم تتغير المدخلات -->
</div>
`
})
export class UserListComponent {
users: User[] = [];
}الـ HTTP Requests هي واحدة من أكبر عنق الزجاجة في أداء تطبيقات Angular، خاصة عندما تتعامل مع كميات كبيرة من البيانات. المشكلة ليست فقط في زمن الاستجابة (Latency)، بل أيضاً في كمية البيانات التي يتم تحميلها. في أحد المشاريع التي عملت عليها، كان التطبيق يقوم بتحميل ٥ ميجابايت من بيانات المنتجات في كل مرة يفتح فيها المستخدم صفحة المنتجات، على الرغم من أن المستخدم لا يرى سوى ١٠ منتجات في الصفحة الواحدة. هذا النوع من الـ Over-fetching يؤدي إلى زيادة زمن التحميل واستهلاك غير ضروري للبيانات، خاصة على الهواتف المحمولة.
الحل هنا هو استخدام تقنيات مثل الـ Pagination، الـ Lazy Loading، والـ GraphQL بدلاً من الـ REST التقليدي. الـ Pagination يسمح لك بتحميل البيانات على دفعات، بدلاً من تحميل كل شيء مرة واحدة. الـ Lazy Loading يسمح لك بتحميل البيانات فقط عندما يحتاجها المستخدم، مثلاً عند التمرير إلى نهاية القائمة. أما الـ GraphQL، فيمكنك من طلب البيانات التي تحتاجها بالضبط، دون الحاجة إلى تحميل حقول غير ضرورية. في المثال التالي، سأريك كيف يمكن تحويل طلب HTTP بسيط إلى طلب ذكي يستخدم الـ Pagination والـ Lazy Loading لتحسين الأداء بشكل كبير.
// قبل: تحميل جميع البيانات مرة واحدة
@Injectable()
export class ProductService {
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products'); // تحميل جميع المنتجات
}
}
// بعد: استخدام Pagination و Lazy Loading
@Injectable()
export class ProductService {
private currentPage = 1;
private pageSize = 10;
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products', {
params: {
page: this.currentPage.toString(),
limit: this.pageSize.toString()
}
});
}
loadMore() {
this.currentPage++;
return this.getProducts();
}
}
// في المكون
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products">
{{ product.name }}
</div>
<button (click)="loadMore()">تحميل المزيد</button>
`
})
export class ProductListComponent {
products: Product[] = [];
constructor(private productService: ProductService) {
this.loadProducts();
}
loadProducts() {
this.productService.getProducts().subscribe(products => {
this.products = [...this.products, ...products]; // إضافة المنتجات الجديدة إلى القائمة
});
}
loadMore() {
this.productService.loadMore().subscribe(products => {
this.products = [...this.products, ...products];
});
}
}لكن الـ Pagination ليس الحل الوحيد. يمكنك أيضاً استخدام الـ Intersection Observer API لمراقبة متى يصل المستخدم إلى نهاية القائمة، ثم تحميل البيانات تلقائياً دون الحاجة إلى زر "تحميل المزيد". هذه التقنية تستخدم بشكل واسع في تطبيقات مثل فيسبوك وإنستغرام، حيث يتم تحميل المحتوى تلقائياً عند التمرير. أيضاً، يمكنك استخدام الـ Service Worker لتخزين البيانات مؤقتاً في الـ Cache، مما يقلل من الحاجة إلى إرسال طلبات HTTP متكررة. في المثال التالي، سأريك كيف يمكنك استخدام الـ Intersection Observer مع Angular لتحميل البيانات تلقائياً عند التمرير.
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products">
{{ product.name }}
</div>
<div #loader></div>
`
})
export class ProductListComponent implements AfterViewInit, OnDestroy {
products: Product[] = [];
private currentPage = 1;
private pageSize = 10;
private observer: IntersectionObserver;
@ViewChild('loader') loader: ElementRef;
constructor(private productService: ProductService) {}
ngAfterViewInit() {
this.observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
this.loadMore();
}
});
this.observer.observe(this.loader.nativeElement);
}
ngOnDestroy() {
this.observer.disconnect();
}
loadMore() {
this.productService.getProducts(this.currentPage, this.pageSize).subscribe(products => {
this.products = [...this.products, ...products];
this.currentPage++;
});
}
}عندما نتحدث عن أداء تطبيقات Angular، فإننا غالباً ما نركز على زمن التحميل واستهلاك الذاكرة، لكننا ننسى أن تجربة المستخدم الحقيقية تعتمد على سلاسة الـ Rendering. الـ Jank (التقطيع في الحركة) يحدث عندما لا يستطيع المتصفح تحديث الشاشة بمعدل ٦٠ إطار في الثانية، مما يؤدي إلى تجربة متقطعة وغير مريحة. في أحد المشاريع، كان التطبيق يعاني من تقطيع واضح عند التمرير في قوائم المنتجات، على الرغم من أن البيانات كانت محملة مسبقاً. بعد تحليل الأداء باستخدام أدوات مثل Chrome DevTools، اكتشفنا أن المشكلة كانت في إعادة رسم الـ DOM بشكل متكرر بسبب استخدام الـ ngFor مع عناصر معقدة، مما يؤدي إلى ما يسمى بـ "الـ Layout Thrashing".
الحل الأول والأكثر فعالية هو تقليل عدد عمليات الـ DOM Manipulation. بدلاً من إعادة رسم القائمة بأكملها في كل مرة تتغير فيها البيانات، يمكنك استخدام تقنيات مثل الـ Virtual Scrolling لعرض جزء صغير فقط من القائمة في أي وقت. هذا يقلل بشكل كبير من عدد العناصر التي يتم رسمها، مما يحسن أداء التمرير بشكل ملحوظ. أيضاً، يمكنك استخدام الـ trackBy مع الـ ngFor لتجنب إعادة إنشاء عناصر الـ DOM عند تغيير البيانات. في المثال التالي، سأريك كيف يمكن أن يؤدي استخدام الـ trackBy إلى تحسين أداء قوائم الـ ngFor بشكل كبير، خاصة عندما تحتوي القائمة على عناصر معقدة.
// قبل: إعادة إنشاء عناصر DOM عند تغيير البيانات
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products">
<app-product-card [product]="product"></app-product-card>
</div>
`
})
export class ProductListComponent {
products: Product[] = [];
}
// بعد: استخدام trackBy لتجنب إعادة إنشاء عناصر DOM
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products; trackBy: trackById">
<app-product-card [product]="product"></app-product-card>
</div>
`
})
export class ProductListComponent {
products: Product[] = [];
trackById(index: number, product: Product): number {
return product.id; // استخدام معرف فريد لتتبع العناصر
}
}لكن الـ trackBy ليس الحل الوحيد. يمكنك أيضاً استخدام مكتبات مثل @ngx-virtual-scroller لتنفيذ الـ Virtual Scrolling في قوائمك الكبيرة. هذه المكتبة تسمح لك بعرض جزء صغير فقط من القائمة في أي وقت، مما يقلل عدد عناصر الـ DOM المرسومة بشكل كبير. أيضاً، يمكنك استخدام الـ CSS Containment لعزل أجزاء معينة من الصفحة ومنع إعادة رسمها عند تغيير أجزاء أخرى. في المثال التالي، سأريك كيف يمكنك استخدام @ngx-virtual-scroller لتنفيذ قائمة افتراضية تعمل بسلاسة حتى مع آلاف العناصر.
@Component({
selector: 'app-product-list',
template: `
<virtual-scroller [items]="products" (vsUpdate)="onUpdate($event)">
<app-product-card *vsFor="let product of products" [product]="product"></app-product-card>
</virtual-scroller>
`
})
export class ProductListComponent {
products: Product[] = [];
constructor(private productService: ProductService) {
this.productService.getProducts().subscribe(products => {
this.products = products;
});
}
onUpdate(event: any) {
// يمكن استخدام هذا الحدث لتحميل المزيد من البيانات عند الحاجة
}
}أيضاً، يجب أن تكون حذراً مع استخدام الـ Animations في Angular. على الرغم من أن الـ Animations يمكن أن تجعل التطبيق يبدو أكثر احترافية، إلا أنها يمكن أن تؤدي إلى تقطيع في الحركة إذا لم يتم تنفيذها بعناية. المفتاح هنا هو استخدام الـ transform و opacity بدلاً من الخصائص التي تؤدي إلى إعادة حساب الـ Layout مثل width و height. أيضاً، يجب تجنب تشغيل أكثر من animation في نفس الوقت، لأن هذا يمكن أن يؤدي إلى زيادة الحمل على الـ GPU. في المثال التالي، سأريك كيف يمكنك تنفيذ animation سلسة باستخدام الـ transform بدلاً من الخصائص التي تؤدي إلى إعادة حساب الـ Layout.
@Component({
selector: 'app-product-card',
template: `
<div [@fadeIn]="state" class="product-card">
{{ product.name }}
</div>
`,
animations: [
trigger('fadeIn', [
state('void', style({
opacity: 0,
transform: 'translateY(20px)'
})),
state('*', style({
opacity: 1,
transform: 'translateY(0)'
})),
transition('void => *', animate('300ms ease-out'))
])
]
})
export class ProductCardComponent {
@Input() product: Product;
state = 'active';
}حجم الـ Bundle في تطبيقات Angular يمكن أن يكون كارثة حقيقية للأداء، خاصة على الهواتف المحمولة وشبكات الـ 3G. في أحد المشاريع، كان حجم الـ Bundle الأساسي للتطبيق يتجاوز ٥ ميجابايت، مما يؤدي إلى زمن تحميل يتجاوز ١٥ ثانية على شبكات بطيئة. المشكلة الأكبر أن هذا الحجم الكبير لا يؤثر فقط على زمن التحميل الأولي، بل أيضاً على استهلاك الذاكرة واستجابة التطبيق بشكل عام. عندما قمت بتحليل الـ Bundle باستخدام أدوات مثل webpack-bundle-analyzer، اكتشفت أن أكثر من ٦٠٪ من الحجم كان يأتي من مكتبات خارجية غير ضرورية، مثل moment.js التي كانت تستهلك أكثر من ٣٠٠ كيلوبايت لوحدها.
الحل الأول والأكثر فعالية هو استخدام الـ Lazy Loading لتقسيم التطبيق إلى أجزاء صغيرة يتم تحميلها عند الحاجة فقط. بدلاً من تحميل جميع مكونات التطبيق في الـ Bundle الرئيسي، يمكنك تحميل كل وحدة (Module) بشكل مستقل عند دخول المستخدم إليها. هذا يقلل بشكل كبير من حجم الـ Bundle الأولي، مما يحسن زمن التحميل بشكل ملحوظ. أيضاً، يمكنك استخدام الـ Tree Shaking لإزالة الكود غير المستخدم من الـ Bundle النهائي. في المثال التالي، سأريك كيف يمكنك إعداد الـ Lazy Loading في تطبيق Angular لتقليل حجم الـ Bundle الأولي.
// في ملف app-routing.module.ts
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }لكن الـ Lazy Loading ليس الحل الوحيد. يمكنك أيضاً استخدام مكتبات أخف بدلاً من المكتبات الثقيلة. مثلاً، بدلاً من استخدام moment.js لإدارة التواريخ، يمكنك استخدام date-fns أو حتى الـ Date API المدمج في جافاسكريبت. أيضاً، يمكنك استخدام الـ Ivy Compiler في Angular، الذي يقلل حجم الـ Bundle بشكل كبير مقارنة بالـ View Engine القديم. في المثال التالي، سأريك كيف يمكنك استبدال moment.js بـ date-fns لتقليل حجم الـ Bundle بأكثر من ٣٠٠ كيلوبايت.
// قبل: استخدام moment.js
import * as moment from 'moment';
const date = moment().format('YYYY-MM-DD');
// بعد: استخدام date-fns
import { format } from 'date-fns';
const date = format(new Date(), 'yyyy-MM-dd');أيضاً، يجب أن تكون حذراً مع استخدام الصور والوسائط في تطبيقك. الصور الكبيرة يمكن أن تزيد حجم الـ Bundle بشكل كبير، خاصة إذا كانت مضمنة مباشرة في الكود. الحل هنا هو استخدام أدوات مثل ImageOptim لضغط الصور، واستخدام تنسيقات حديثة مثل WebP بدلاً من JPEG أو PNG. أيضاً، يمكنك استخدام الـ Lazy Loading للصور، بحيث يتم تحميلها فقط عندما تظهر في الشاشة. في المثال التالي، سأريك كيف يمكنك استخدام الـ Intersection Observer لتحميل الصور بشكل كسول، مما يحسن زمن التحميل بشكل كبير.
@Component({
selector: 'app-product-card',
template: `
<img [src]="imageSrc" (load)="onImageLoad()" #image>
`
})
export class ProductCardComponent implements AfterViewInit, OnDestroy {
@ViewChild('image') image: ElementRef;
imageSrc = '';
private observer: IntersectionObserver;
ngAfterViewInit() {
this.observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
this.imageSrc = this.product.imageUrl;
this.observer.disconnect();
}
});
this.observer.observe(this.image.nativeElement);
}
ngOnDestroy() {
this.observer.disconnect();
}
onImageLoad() {
// يمكن استخدام هذا الحدث لإظهار الصورة بعد تحميلها
}
}عندما نتحدث عن أداء تطبيقات Angular، فإننا غالباً ما نركز على تحسين الـ UI وسرعة الـ Rendering، لكننا ننسى أن العمليات الحسابية الثقيلة يمكن أن تؤدي إلى تجمد التطبيق بشكل كامل. على سبيل المثال، إذا كان لديك مكون يقوم بحساب تقارير معقدة أو معالجة صور كبيرة، فإن هذه العمليات ستشغل الـ Main Thread في المتصفح، مما يمنع الـ UI من الاستجابة للمستخدم. في أحد المشاريع، كان التطبيق يتجمد تماماً عند تحميل تقرير يحتوي على آلاف الصفوف، لأن الحسابات كانت تتم في الـ Main Thread. الحل هنا هو استخدام الـ Web Workers لنقل هذه العمليات بعيداً عن الـ Main Thread، مما يسمح للتطبيق بالاستمرار في الاستجابة للمستخدم حتى أثناء تنفيذ العمليات الثقيلة.
الـ Web Workers هي تقنية تسمح لك بتشغيل كود جافاسكريبت في خلفية منفصلة عن الـ Main Thread. هذا يعني أنه يمكنك تنفيذ العمليات الحسابية الثقيلة دون التأثير على أداء الـ UI. في Angular، يمكنك استخدام مكتبة مثل @angular/platform-webworker لتنفيذ الـ Web Workers بسهولة. في المثال التالي، سأريك كيف يمكنك استخدام الـ Web Worker لمعالجة بيانات كبيرة دون تجميد الـ UI.
// worker.ts
self. (event) => {
const data = event.data;
const result = heavyCalculation(data); // عملية حسابية ثقيلة
self.postMessage(result);
};
function heavyCalculation(data: any): any {
// تنفيذ الحسابات المعقدة هنا
return processedData;
}
// في المكون
@Component({
selector: 'app-report',
template: `...
})
export class ReportComponent {
private worker: Worker;
constructor() {
this.worker = new Worker('./report.worker', { type: 'module' });
this.worker.onmessage = ({ data }) => {
console.log('Result from worker:', data);
};
}
generateReport() {
this.worker.postMessage(this.reportData);
}
ngOnDestroy() {
this.worker.terminate();
}
}لكن استخدام الـ Web Workers ليس دائماً الحل الأمثل. هناك بعض القيود التي يجب أن تكون على دراية بها. أولاً، الـ Web Workers لا يمكنها الوصول إلى الـ DOM مباشرة، مما يعني أنك لا تستطيع تحديث الـ UI من داخل الـ Worker. ثانياً، التواصل بين الـ Main Thread والـ Worker يتم عبر الرسائل، مما يضيف بعض الـ Overhead. ثالثاً، ليس كل العمليات تستحق نقلها إلى الـ Worker، لأن إنشاء وإدارة الـ Worker نفسه له تكلفة. القاعدة العامة هنا هي: إذا كانت العملية تستغرق أكثر من ٥٠ مللي ثانية، فقد يكون من الجيد نقلها إلى الـ Worker. في المثال التالي، سأريك كيف يمكنك استخدام الـ Web Worker لمعالجة صور كبيرة دون تجميد الـ UI.
// worker.ts
self. (event) => {
const imageData = event.data;
const processedImage = processImage(imageData); // معالجة الصورة
self.postMessage(processedImage);
};
function processImage(imageData: ImageData): ImageData {
// تنفيذ معالجة الصورة هنا
return processedImageData;
}
// في المكون
@Component({
selector: 'app-image-editor',
template: `...
})
export class ImageEditorComponent {
private worker: Worker;
constructor() {
this.worker = new Worker('./image.worker', { type: 'module' });
this.worker.onmessage = ({ data }) => {
this.displayProcessedImage(data);
};
}
processImage(imageData: ImageData) {
this.worker.postMessage(imageData);
}
displayProcessedImage(imageData: ImageData) {
// تحديث الـ UI بالصورة المعالجة
}
ngOnDestroy() {
this.worker.terminate();
}
}تحسين أداء تطبيقات Angular ليس عملية تحدث مرة واحدة ثم تنتهي. الأداء هو شيء يجب مراقبته باستمرار، خاصة مع تطور التطبيق وإضافة ميزات جديدة. المشكلة أن العديد من المطورين يعتمدون على الحدس أو الاختبارات المحلية لتحسين الأداء، بدلاً من الاعتماد على بيانات حقيقية من المستخدمين النهائيين. في أحد المشاريع، قمنا بإضافة ميزة جديدة للتطبيق، وكان كل شيء يبدو سريعاً في بيئة التطوير، لكن عند إطلاق الميزة للمستخدمين، اكتشفنا أن زمن التحميل زاد بنسبة ٤٠٪ على الهواتف المحمولة. السبب؟ لم نكن نرصد الأداء في بيئات حقيقية، حيث تختلف سرعة الشبكة وقدرات الأجهزة بشكل كبير عن بيئة التطوير.
الحل هنا هو استخدام أدوات مراقبة الأداء مثل Google Lighthouse، WebPageTest، وNew Relic لجمع بيانات حقيقية عن أداء التطبيق في بيئات الإنتاج. هذه الأدوات تسمح لك بقياس زمن التحميل، استهلاك الذاكرة، وعدد عمليات الـ Rendering في بيئات حقيقية، مما يساعدك على تحديد عنق الزجاجة الحقيقي في التطبيق. أيضاً، يمكنك استخدام الـ Angular Performance Profiler لمراقبة أداء مكونات معينة في التطبيق. في المثال التالي، سأريك كيف يمكنك استخدام Google Lighthouse لمراقبة أداء تطبيق Angular وتحسينه بناءً على البيانات الحقيقية.
# تثبيت Lighthouse
npm install -g lighthouse
# تشغيل Lighthouse على تطبيق Angular
lighthouse http://localhost:4200 --output=html --output-path=./report.htmlلكن مراقبة الأداء ليست مجرد جمع البيانات، بل أيضاً تحليلها واتخاذ قرارات بناءً عليها. مثلاً، إذا اكتشفت أن زمن التحميل الأولي للتطبيق بطيء بسبب حجم الـ Bundle الكبير، فيمكنك التركيز على تحسين الـ Lazy Loading وتقليل حجم الـ Bundle. إذا اكتشفت أن التطبيق يتجمد عند التمرير في قوائم كبيرة، فيمكنك التركيز على تحسين الـ Rendering باستخدام الـ Virtual Scrolling. المفتاح هنا هو أن تكون استباقياً في مراقبة الأداء، بدلاً من الانتظار حتى يشكو المستخدمون من البطء. في المثال التالي، سأريك كيف يمكنك إعداد مراقبة مستمرة للأداء باستخدام GitHub Actions وLighthouse CI.
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- run: npm run build -- --prod
- run: npm install -g @lhci/cli
- run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}تحسين أداء تطبيقات Angular ليس مجرد تطبيق مجموعة من الحيل البرمجية، بل هو فهم عميق لكيفية عمل محرك جافاسكريبت، الـ Event Loop، والـ Rendering Pipeline في المتصفح. كل تقنية ذكرتها في هذا المقال ليست مجرد نصيحة نظرية، بل هي نتيجة تجارب حقيقية في مشاريع كبيرة ومعقدة. المفتاح الحقيقي لتحسين الأداء هو أن تتوقف عن التخمين وتبدأ في القياس. استخدم أدوات مثل Chrome DevTools وLighthouse لقياس أداء التطبيق في بيئات حقيقية، ثم ركز على تحسين الجزء الذي يشكل عنق الزجاجة الحقيقي. أيضاً، لا تنسَ أن الأداء ليس شيئاً تضيفه في النهاية، بل هو جزء من عملية التطوير منذ اليوم الأول. إذا انتظرت حتى نهاية المشروع لتحسين الأداء، فستجد نفسك أمام جبل من المشاكل التي يصعب حلها.
في النهاية، تذكر أن المستخدم النهائي لا يهتم إذا كنت تستخدم Angular أو React أو Vue، بل يهتم بتجربة سلسة وسريعة. إذا كان تطبيقك بطيئاً أو متقطعاً، فلن يبقى المستخدمون طويلاً، بغض النظر عن مدى جمال تصميمه. لذا، اجعل الأداء أولوية منذ اليوم الأول، وقس باستمرار، وافهم ما يحدث خلف الكواليس. بهذه الطريقة، ستحول تطبيقك من تجربة متجمدة إلى تجربة سلسة تجعل المستخدمين يعودون مراراً وتكراراً.