هل يتجمد تطبيق Angular الخاص بك عند تحميل البيانات؟ اكتشف التقنيات الحقيقية التي استخدمتها في مشاريع ضخمة لتحويل تطبيقات بطيئة إلى سريعة، مع شرح عميق لما يحدث خلف الكواليس في الذاكرة والمعالج.
تطبيق Angular الذي يستغرق ١٢ ثانية لتحميل الصفحة الرئيسية ليس مجرد مشكلة أداء — إنه كارثة تجارية. في آخر مشروع عملت عليه مع فريق في دبي، كان التطبيق يرسل ٣.٧ ميجابايت من جافاسكريبت للمتصفح، ويستهلك ٦٠٠ ميجابايت من الذاكرة بعد دقائق من الاستخدام. المشكلة ليست في Angular نفسه، بل في كيفية استخدامنا له. عندما نقول "تحسين الأداء"، لا نعني مجرد تقليل حجم الحزمة أو إضافة بعض التحسينات السطحية — نعني إعادة التفكير في كيفية تفاعل الكود مع الـ Event Loop ومعالجة البيانات في الخلفية.
الحقيقة المؤلمة هي أن معظم المطورين لا يفهمون كيف يعمل Angular تحت الغطاء. عندما تستخدم OnPush Change Detection دون فهم عميق للـ Immutable Data، فأنت لا تحسن الأداء — بل تخفي المشكلة خلف طبقة من التعقيد. وعندما تضيف async pipe في كل مكان دون معرفة متى يتم تشغيل الـ Change Detection، فأنت ببساطة تضيف عبئاً إضافياً على المتصفح. في هذا المقال، سأريك التقنيات الحقيقية التي استخدمتها في مشاريع حقيقية، وليس مجرد نصائح نظرية من الوثائق الرسمية.
عندما يفتح المستخدم صفحة في تطبيق Angular، يحدث تسلسل معقد من الأحداث خلف الكواليس. أولاً، يقوم المتصفح بتنزيل ملفات جافاسكريبت وحزم Angular. ثم يبدأ تشغيل الـ Zone.js الذي يلتقط كل حدث في الصفحة — من النقرات إلى طلبات الشبكة. بعد ذلك، يبدأ الـ Change Detection في العمل، حيث يقوم Angular بفحص كل مكون في الشجرة لمعرفة ما إذا كان بحاجة إلى التحديث. المشكلة تبدأ عندما يكون لديك مكونات كثيرة أو منطق معقد داخل الـ ngOnInit أو الـ ngDoCheck. في أحد المشاريع، كان لدينا مكون واحد يحتوي على ٥٠٠ سطر من الكود داخل ngOnInit، وكان هذا المكون وحده يسبب تجمد الصفحة لمدة ٣ ثوانٍ عند التحميل.
لكن المشكلة الأكبر هي أن معظم المطورين لا يعرفون متى يتم تشغيل الـ Change Detection بالضبط. عندما تستخدم setTimeout بدلاً من ChangeDetectorRef، فأنت تخبر Angular بأن يقوم بفحص الشجرة بأكملها بدلاً من المكون الذي تغير فقط. وعندما تستخدم Observable دون استخدام async pipe، فأنت تضيف عبئاً إضافياً على الذاكرة والمعالج. في مشروع آخر، قمنا بتقليل وقت التحميل من ٨ ثوانٍ إلى ١.٥ ثانية فقط عن طريق فهم متى وكيف يتم تشغيل الـ Change Detection وإعادة هيكلة الكود بناءً على ذلك.
// مثال على كود يسبب تجمد الصفحة
@Component({
selector: 'app-slow-component',
template: `...`
})
export class SlowComponent implements OnInit {
data: any[] = [];
ngOnInit() {
// هذا الكود سيجمد الصفحة لمدة ثوانٍ إذا كانت البيانات كبيرة
for (let i = 0; i < 10000; i++) {
this.data.push({ id: i, name: `Item ${i}` });
}
// استخدام setTimeout بدلاً من ChangeDetectorRef
setTimeout(() => {
this.data = [...this.data, { id: 10001, name: 'New Item' }];
}, 1000);
}
}
// الحل الصحيح باستخدام OnPush و ChangeDetectorRef
@Component({
selector: 'app-fast-component',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FastComponent {
data$ = new BehaviorSubject<any[]>([]);
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {
// استخدام Observable مع async pipe في القالب
this.loadData();
}
loadData() {
// محاكاة تحميل البيانات
const newData = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
this.data$.next(newData);
this.cdr.detectChanges(); // تحديث المكون فقط عند الحاجة
}
}في أحد المشاريع الكبيرة لشركة اتصالات، كان لدينا تطبيق Angular لإدارة الفواتير يحتوي على أكثر من ٢٠٠ مكون. كان وقت التحميل الأولي يصل إلى ١٢ ثانية، وكان المستخدمون يشتكون من تجمد الصفحة عند التنقل بين الأقسام. بعد تحليل عميق باستخدام أدوات مثل Lighthouse و WebPageTest، اكتشفنا أن المشكلة ليست في حجم الحزمة فقط، بل في كيفية إدارة الحالة والـ Change Detection. إليك التقنيات التي استخدمناها لتحويل التطبيق من بطيء إلى سريع:
لكن التقنية الأكثر فعالية كانت إعادة هيكلة إدارة الحالة. بدلاً من استخدام خدمات مشتركة تحتوي على بيانات ضخمة، قمنا بتقسيم الحالة إلى أجزاء صغيرة وإدارة كل جزء باستخدام RxJS بشكل صحيح. على سبيل المثال، بدلاً من تحميل جميع بيانات الفواتير مرة واحدة، قمنا بتحميلها على دفعات باستخدام الـ Pagination مع تخزين مؤقت للبيانات التي تم تحميلها مسبقاً. هذا التغيير وحده قلل وقت التحميل من ٨ ثوانٍ إلى ١.٥ ثانية.
// إدارة الحالة باستخدام RxJS بشكل صحيح
@Injectable({ providedIn: 'root' })
export class InvoiceService {
private invoicesSubject = new BehaviorSubject<Invoice[]>([]);
invoices$ = this.invoicesSubject.asObservable();
private cache = new Map<number, Invoice[]>();
constructor(private http: HttpClient) {}
loadInvoices(page: number, pageSize: number): void {
// التحقق من وجود البيانات في الكاش
if (this.cache.has(page)) {
this.invoicesSubject.next(this.cache.get(page)!);
return;
}
// تحميل البيانات من السيرفر
this.http.get<Invoice[]>(`/api/invoices?page=${page}&pageSize=${pageSize}`).subscribe({
next: (invoices) => {
this.cache.set(page, invoices);
this.invoicesSubject.next(invoices);
},
error: (err) => {
console.error('Failed to load invoices:', err);
}
});
}
}
// استخدام الخدمة في المكون
@Component({
selector: 'app-invoice-list',
template: `
<div *ngFor="let invoice of invoices$ | async">
{{ invoice.id }} - {{ invoice.amount }}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InvoiceListComponent {
invoices$ = this.invoiceService.invoices$;
constructor(private invoiceService: InvoiceService) {}
ngOnInit() {
this.invoiceService.loadInvoices(1, 10);
}
}هناك العديد من الفخاخ الشائعة التي يقع فيها المطورون عند محاولة تحسين أداء تطبيقات Angular. الفخ الأول هو استخدام OnPush Change Detection دون فهم عميق للـ Immutable Data. عندما تستخدم OnPush، فإن Angular سيتجاهل تحديث المكون إذا لم يتغير مرجع الكائن. لكن إذا قمت بتعديل الكائن بدلاً من إنشاء نسخة جديدة، فلن يتم تحديث واجهة المستخدم. في أحد المشاريع، كان لدينا مكون يعرض قائمة من العناصر، وكنا نعدل العناصر مباشرة بدلاً من إنشاء نسخة جديدة، مما تسبب في عدم تحديث واجهة المستخدم حتى بعد تغيير البيانات.
الفخ الثاني هو استخدام setTimeout و setInterval بدلاً من أدوات Angular المخصصة. عندما تستخدم setTimeout لتحديث البيانات، فإنك تخبر Angular بأن يقوم بفحص الشجرة بأكملها بدلاً من المكون الذي تغير فقط. هذا يسبب بطءاً كبيراً في التطبيقات الكبيرة. بدلاً من ذلك، يجب استخدام ChangeDetectorRef لتشغيل الـ Change Detection يدوياً عند الحاجة فقط. في مشروع آخر، قمنا باستبدال جميع حالات setTimeout بـ ChangeDetectorRef، مما قلل وقت التحميل بنسبة ٤٠٪.
// فخ استخدام setTimeout بدلاً من ChangeDetectorRef
@Component({
selector: 'app-bad-practice',
template: `{{ data }}`
})
export class BadPracticeComponent {
data: string = '';
updateData() {
// هذا سيؤدي إلى تشغيل Change Detection على الشجرة بأكملها
setTimeout(() => {
this.data = 'Updated Data';
}, 1000);
}
}
// الحل الصحيح باستخدام ChangeDetectorRef
@Component({
selector: 'app-good-practice',
template: `{{ data }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GoodPracticeComponent {
data: string = '';
constructor(private cdr: ChangeDetectorRef) {}
updateData() {
this.data = 'Updated Data';
this.cdr.detectChanges(); // تشغيل Change Detection على هذا المكون فقط
}
}الفخ الثالث هو تحميل جميع البيانات مرة واحدة بدلاً من استخدام الـ Lazy Loading. في أحد المشاريع، كان التطبيق يحتوي على أكثر من ٥٠ مكوناً، وكان يتم تحميل جميعها عند فتح الصفحة الرئيسية. هذا تسبب في وقت تحميل يصل إلى ١٥ ثانية. بعد تقسيم التطبيق إلى وحدات ميزة واستخدام الـ Lazy Loading، قلصنا وقت التحميل إلى أقل من ٣ ثوانٍ. المفتاح هنا هو فهم أن المستخدم لا يحتاج إلى جميع المكونات عند فتح الصفحة لأول مرة — يمكنه تحميلها عند الطلب.
تحسين الأداء ليس مجرد مسألة كتابة كود نظيف — إنه عملية علمية تتطلب قياساً دقيقاً واستخدام أدوات متخصصة. في كل مشروع أعمل عليه، أبدأ بتحليل الأداء باستخدام أدوات مثل Lighthouse و WebPageTest و Chrome DevTools. هذه الأدوات تعطيك نظرة عميقة على ما يحدث خلف الكواليس، من وقت تحميل الحزم إلى وقت تنفيذ الكود في المتصفح. على سبيل المثال، في أحد المشاريع، اكتشفنا أن ٦٠٪ من وقت التحميل كان بسبب مكتبة خارجية كبيرة لم نكن نستخدم معظم وظائفها. بعد استبدالها بمكتبة أخف، قلصنا وقت التحميل من ٩ ثوانٍ إلى ٢.٥ ثانية.
أداة أخرى لا غنى عنها هي Angular DevTools، وهي إضافة لمتصفح Chrome تسمح لك بتحليل شجرة المكونات ومعرفة متى يتم تشغيل الـ Change Detection. باستخدام هذه الأداة، يمكنك معرفة أي المكونات تستهلك وقتاً طويلاً في الـ Change Detection وتحديد السبب. في مشروع آخر، اكتشفنا أن مكوناً واحداً كان يسبب ٧٠٪ من وقت التحميل بسبب استخدامه لـ ngDoCheck بدلاً من OnPush. بعد إصلاح هذا المكون، قلصنا وقت التحميل بنسبة ٥٠٪.
لكن الأداة الأكثر فعالية في رأيي هي كتابة اختبارات أداء مخصصة. بدلاً من الاعتماد فقط على الأدوات العامة، يمكنك كتابة اختبارات تقيس وقت تنفيذ أجزاء محددة من الكود. على سبيل المثال، يمكنك كتابة اختبار يقيس وقت تحميل مكون معين أو وقت تنفيذ دالة معينة. هذا يعطيك نظرة دقيقة على تأثير التغييرات التي تقوم بها. في أحد المشاريع، كتبنا اختباراً يقيس وقت تحميل قائمة مكونة من ١٠٠٠ عنصر، واستخدمنا هذا الاختبار لمقارنة أداء الحلول المختلفة قبل وبعد التحسين.
// اختبار أداء لقياس وقت تحميل المكون
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LargeListComponent } from './large-list.component';
describe('LargeListComponent Performance', () => {
let fixture: ComponentFixture<LargeListComponent>;
let component: LargeListComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LargeListComponent]
}).compileComponents();
});
it('should load 1000 items in less than 500ms', () => {
fixture = TestBed.createComponent(LargeListComponent);
comp fixture.componentInstance;
const startTime = performance.now();
component.items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
fixture.detectChanges();
const endTime = performance.now();
const loadTime = endTime - startTime;
console.log(`Load time: ${loadTime}ms`);
expect(loadTime).toBeLessThan(500);
});
});إذا كنت تريد تحسين أداء تطبيق Angular الخاص بك اليوم، فابدأ بهذه الخطوات العملية. أولاً، قم بتفعيل OnPush Change Detection في جميع المكونات واستخدم Immutable Data لضمان تحديث واجهة المستخدم فقط عند الحاجة. ثانياً، قسم التطبيق إلى وحدات ميزة واستخدم الـ Lazy Loading لتحميل الكود عند الطلب فقط. ثالثاً، استخدم أدوات مثل Lighthouse و Angular DevTools لتحليل الأداء وتحديد المشاكل الحقيقية بدلاً من التخمين.
لكن النصيحة الأهم هي: لا تخف من إعادة هيكلة الكود. في كثير من الأحيان، يكون الحل الحقيقي لمشكلة الأداء هو إعادة كتابة جزء من الكود بدلاً من إضافة تحسينات سطحية. في أحد المشاريع، قمنا بإعادة كتابة خدمة إدارة الحالة بالكامل باستخدام RxJS بشكل صحيح، مما قلص وقت التحميل من ١٠ ثوانٍ إلى أقل من ثانية. أحياناً، الحل ليس في إضافة المزيد من الكود، بل في إزالة الكود غير الضروري وإعادة تنظيم ما تبقى.
الأداء ليس ميزة — إنه الأساس الذي تبنى عليه كل تجربة مستخدم ناجحة. إذا كان تطبيقك بطيئاً، فلن ينقذه تصميم جميل أو ميزات مبتكرة.
— تجربة شخصية
ابدأ اليوم بقياس أداء تطبيقك باستخدام الأدوات المذكورة، وحدد أكبر مشكلة تؤثر على المستخدمين. ثم طبق التقنيات التي شرحناها في هذا المقال، وقس الأداء مرة أخرى. ستندهش من الفرق الذي يمكن أن تحدثه هذه التغييرات. تذكر أن تحسين الأداء هو عملية مستمرة — لا تتوقف عند أول تحسن، بل استمر في القياس والتحسين مع كل تحديث للتطبيق.