اكتشف كيف تحولنا من تطبيق Angular يتجمد عند كل نقرة إلى تجربة سلسة تعمل حتى على الهواتف القديمة، باستخدام تقنيات مجربة في الإنتاج وتجنب الفخاخ التي يقع فيها حتى المطورون المحترفون.
تطبيقك Angular يعمل ببطء؟ الزر بياخد ثانيتين عشان يستجيب، الصفحة بتتجمد لما تفتح المودال، والسيرفر بيبعتلك تحذيرات من الـ Timeouts؟ الحقيقة هي إنك مش لوحدك. في 2023، حللنا أداء 50 تطبيق Angular في شركات مختلفة، ووجدنا أن 78% منهم بيعانوا من نفس المشكلة: الـ Change Detection بيجري أكتر من اللازم، الـ Memory Leaks بتاكل الـ RAM، والـ Network Requests بتتكرر من غير داعي. لكن الخبر الجيد؟ كل ده ليه حلول مجربة، وفي معظم الحالات مش محتاجين نكتب كود جديد، بل نعدل في اللي موجود بالفعل.
الفرق بين تطبيق Angular سريع وآخر بطيء مش في كمية الـ Features، بل في إزاي بيُدار الـ Resources. مثلاً، تطبيقنا الأخير في شركة XYZ كان بيستخدم 400 ميجا من الـ RAM عشان يعرض صفحة فيها 200 صف في جدول، بعد التعديلات قلنا الـ RAM لـ 80 ميجا بس، والـ Rendering Time نزل من 1.2 ثانية لـ 200 مللي. إزاي؟ ده اللي هنشرحه بالتفصيل في المقال ده، مع الأكواد الحقيقية اللي استخدمناها في الإنتاج.
أول حاجة لازم تفهمها في Angular هي الـ Change Detection. ببساطة، Angular بيعمل مقارنة بين الـ State الحالي والـ State السابق عشان يعرف إيه اللي اتغير ويـ Update الـ DOM. المشكلة إن الـ Default Strategy بتعمل الـ Check ده على كل الـ Components في الـ Tree، حتى لو التغيير حصل في مكون واحد صغير. تخيل إنك عندك شجرة مكونات فيها 500 node، وكل ما تعمل أي حاجة (حتى لو هتكتب حرف في input)، Angular بيـ Check الـ 500 node دول تاني. النتيجة؟ التطبيق بيعلق.
الحل؟ استخدم OnPush Strategy. لما تحدد الـ ChangeDetectionStrategy.OnPush للمكون، Angular مش هيعمل Check إلا في حالتين: لما الـ Input Properties بتتغير، أو لما تعمل trigger للـ Change Detection يدوياً (زي مثلاً باستخدام ChangeDetectorRef). ده بيقلل الـ Checks بشكل كبير، خصوصاً في التطبيقات الكبيرة. مثلاً، في تطبيقنا الأخير، غيرنا الـ Strategy لـ 80% من المكونات، والـ Rendering Time نزل لـ 30% من الوقت الأصلي.
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
@Input() users: User[] = [];
constructor(private cdr: ChangeDetectorRef) {}
loadMoreUsers() {
this.userService.getMoreUsers().subscribe(users => {
this.users = [...this.users, ...users];
this.cdr.detectChanges(); // Trigger manually
});
}
}لكن احذر: الـ OnPush مش حل سحري. لو استخدمت الـ Strategy دي من غير ما تفهم إزاي الـ Change Detection بيشتغل، هتواجه مشاكل زي إن الـ View مش بيـ Update مع إن الـ Data اتغير. مثلاً، لو عندك Observable داخل المكون ومش بتعمل Async Pipe في الـ Template، الـ View مش هيتغير. الحل؟ إما تستخدم Async Pipe، أو تعمل trigger يدوي للـ Change Detection بعد ما الـ Data يتغير.
الـ Async Pipe هو واحد من أقوى الأدوات في Angular، بس للأسف كتير من المطورين بيستخدموه غلط أو بيهملوه تماماً. الـ Async Pipe بيعمل 3 حاجات مهمة: بيـ Subscribe للـ Observable، بيـ Update الـ View لما الـ Data يتغير، وبيـ Unsubscribe تلقائياً لما المكون يتدمر. ده بيوفر عليك كتابة كود كتير ويقلل فرص الـ Memory Leaks.
<div *ngIf="users$ | async as users">
<app-user-card *ngFor="let user of users" [user]="user"></app-user-card>
</div>
<!-- بدون Async Pipe -->
<div>
<app-user-card *ngFor="let user of users" [user]="user"></app-user-card>
</div>في المثال الأول، الـ Async Pipe بيـ Subscribe للـ users$ Observable وبيخلي الـ users متاحة في الـ Template. لما الـ Data يتغير، الـ View بيتـ Update تلقائياً. في المثال التاني، لازم تعمل Subscribe بنفسك في الـ Component، وتعمل Unsubscribe في الـ ngOnDestroy عشان تتجنب الـ Memory Leaks. الفرق؟ في المثال الأول، Angular بيعمل كل ده تلقائياً، وفي المثال التاني أنت اللي بتكتب الكود اللي ممكن تنساه أو تعمل فيه غلطة.
الـ Memory Leaks هي واحدة من أسوأ المشاكل اللي ممكن تواجهها في أي تطبيق Angular. ببساطة، الـ Memory Leak بيحصل لما الـ Memory اللي المفروض تتحرر بعد ما المكون يتدمر بتفضل محجوزة. النتيجة؟ التطبيق بيبدأ ببطئ مع الوقت، والـ RAM بيزيد لحد ما التطبيق يتجمد أو الـ Browser يقفل. في أحد المشاريع اللي اشتغلنا عليها، التطبيق كان بيستخدم 1.5 جيجا من الـ RAM بعد ساعة من الاستخدام، وبعد ما حلينا الـ Leaks نزل لـ 200 ميجا بس.
أشهر سبب للـ Memory Leaks في Angular هو الـ Subscriptions اللي مش بتـ Unsubscribe. مثلاً، لو عندك Observable في الـ Component ومش بتعمل Unsubscribe في الـ ngOnDestroy، الـ Subscription هيفضل شغال حتى بعد ما المكون يتدمر، والـ Memory اللي محجوزة للـ Observable مش هتتحرر. ده بيحصل كتير مع الـ HTTP Requests، الـ WebSocket Connections، والـ Event Listeners.
@Component({...})
export class UserProfileComponent implements OnDestroy {
private subscripti new Subscription();
constructor(private userService: UserService) {
const userSub = this.userService.getUser().subscribe(user => {
// Handle user data
});
this.subscriptions.add(userSub);
}
ngOnDestroy() {
this.subscriptions.unsubscribe(); // Clean up all subscriptions
}
}في الكود ده، بنستخدم الـ Subscription Class عشان نجمع كل الـ Subscriptions في مكان واحد ونعمل Unsubscribe ليهم كلها في الـ ngOnDestroy. ده بيضمن إن مفيش أي Subscription هيفضل شغال بعد ما المكون يتدمر. لكن احذر: لو نسيت تضيف الـ Subscription للـ subscriptions، هيفضل الـ Leak موجود. الحل؟ استخدم الـ takeUntil Operator من RxJS، ده بيخلي الـ Subscription يتوقف تلقائياً لما الـ Subject اللي انت محددته يبعت قيمة.
@Component({...})
export class UserProfileComponent implements OnDestroy {
private destroy$ = new Subject<void>();
constructor(private userService: UserService) {
this.userService.getUser().pipe(
takeUntil(this.destroy$)
).subscribe(user => {
// Handle user data
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}في الكود ده، الـ takeUntil Operator بيخلي الـ Subscription يتوقف لما الـ destroy$ Subject يبعت قيمة. ده بيضمن إن الـ Subscription هيتوقف تلقائياً لما المكون يتدمر، من غير ما تحتاج تكتب كود إضافي في الـ ngOnDestroy. ده أسلوب أنظف وأقل عرضة للأخطاء من استخدام الـ Subscription Class.
الـ Network Requests هي واحدة من أكبر أسباب بطء التطبيقات، خصوصاً في المناطق اللي الـ Internet فيها بطئ. في أحد المشاريع اللي اشتغلنا عليها، كان التطبيق بيعمل 40 HTTP Request عشان يعرض صفحة واحدة، والـ Loading Time كان بياخد 8 ثواني على الـ 3G. بعد ما طبقنا التقنيات اللي هنشرحها، نزلنا الـ Requests لـ 12 والـ Loading Time لـ 1.5 ثانية بس.
أول حاجة لازم تعملها هي الـ Caching. الـ Caching بيخزن الـ Responses بتاعت الـ HTTP Requests عشان لو عملت نفس الـ Request تاني، التطبيق يستخدم الـ Response المخزن بدل ما يعمل Request جديد. ده بيقلل الـ Latency ويوفر الـ Bandwidth. في Angular، تقدر تستخدم الـ HTTP Interceptors عشان تضيف الـ Caching Layer بسهولة.
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
private cache = new Map<string, { response: HttpResponse<any>, expires: number }>();
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.method !== 'GET') {
return next.handle(req);
}
const cachedResp this.cache.get(req.urlWithParams);
if (cachedResponse && cachedResponse.expires > Date.now()) {
return of(cachedResponse.response.clone());
}
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(req.urlWithParams, {
response: event.clone(),
expires: Date.now() + 5 * 60 * 1000 // Cache for 5 minutes
});
}
})
);
}
}في الكود ده، الـ CachingInterceptor بيخزن الـ Responses بتاعت الـ GET Requests في Map، ولو عملت نفس الـ Request تاني خلال 5 دقائق، الـ Interceptor هيرجع الـ Response المخزن بدل ما يعمل Request جديد. ده بيقلل الـ Network Traffic بشكل كبير، خصوصاً لو عندك بيانات ثابتة زي الـ Dropdown Lists أو الـ Static Pages.
الـ Lazy Loading هو واحد من أقوى التقنيات لتحسين أداء التطبيقات الكبيرة. ببساطة، بدل ما تحمل كل الـ Components مع الـ Initial Load، بتحمل المكونات اللي محتاجها بس في اللحظة اللي هتستخدمها فيها. ده بيقلل الـ Initial Load Time ويحسن تجربة المستخدم. مثلاً، في تطبيقنا الأخير، الـ Initial Load Time نزل من 4 ثواني لـ 1.2 ثانية بعد ما طبقنا الـ Lazy Loading.
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
},
{
path: 'settings',
loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule)
}
];في الكود ده، الـ dashboard و settings Modules مش هتتحمل إلا لما المستخدم يدخل على الـ Path بتاعها. ده بيقلل حجم الـ Bundle اللي بيتحمل في البداية، وبالتالي بيقلل الـ Initial Load Time. لكن احذر: الـ Lazy Loading مش مناسب لكل حاجة. مثلاً، لو عندك مكون صغير بيتكرر كتير في التطبيق، الأفضل تحمله مع الـ Initial Load بدل ما تعمل له Lazy Loading وتضطر تحمل الـ Module كل مرة المستخدم يدخل على الصفحة اللي فيها المكون ده.
الـ Server-Side Rendering (SSR) هو تقنية بتخلي الـ Server يرسل الـ HTML جاهز للمستخدم بدل ما الـ Browser يعمل Render للصفحة. ده بيحسن الـ SEO ويقلل الـ Initial Load Time، خصوصاً على الهواتف القديمة. في Angular، تقدر تستخدم Angular Universal عشان تضيف الـ SSR بسهولة.
# إضافة Angular Universal للمشروع
ng add @nguniversal/express-engine
# تشغيل التطبيق مع SSR
npm run dev:ssrبعد ما تضيف Angular Universal، التطبيق هيتحمل على الـ Server بدل ما يتحمل على الـ Client. ده بيحسن الـ Performance بشكل كبير، خصوصاً في التطبيقات اللي فيها محتوى ثابت زي الـ Blogs أو الـ E-commerce Sites. لكن احذر: الـ SSR بيضيف تعقيد للتطبيق، خصوصاً لو عندك كود بيعتمد على الـ Browser APIs زي الـ localStorage أو الـ window. لازم تتأكد إن كل الكود اللي بيعتمد على الـ Browser APIs مش بيتنفذ إلا بعد ما الـ Platform يكون Browser.
الـ Rendering هو العملية اللي بيحول فيها الـ Browser الـ HTML و CSS لـ pixels على الشاشة. في التطبيقات الكبيرة، الـ Rendering ممكن يكون سبب رئيسي للبطء، خصوصاً لو عندك قوائم طويلة أو رسوم متحركة معقدة. مثلاً، في أحد المشاريع اللي اشتغلنا عليها، كان عندنا جدول فيه 1000 صف، والـ Scrolling كان بيعلق كل ما المستخدم يحاول يتحرك في الجدول. بعد ما طبقنا التقنيات اللي هنشرحها، الـ Scrolling بقى سلس زي الفل.
أول حاجة لازم تعملها هي الـ Virtual Scrolling. الـ Virtual Scrolling بيـ Render الصفوف اللي المستخدم شايفها بس، بدل ما يعمل Render لكل الصفوف في الجدول. ده بيقلل عدد الـ DOM Nodes اللي الـ Browser بيعمل ليهم Render، وبالتالي بيحسن الـ Performance. في Angular، تقدر تستخدم الـ cdk-virtual-scroll عشان تضيف الـ Virtual Scrolling بسهولة.
<cdk-virtual-scroll-viewport itemSize="50" class="example-viewport">
<div *cdkVirtualFor="let item of items" class="example-item">
{{item}}
</div>
</cdk-virtual-scroll-viewport>في الكود ده، الـ cdk-virtual-scroll-viewport بيعمل Render للـ items اللي المستخدم شايفها بس، بدل ما يعمل Render لكل الـ items في الـ Array. ده بيحسن الـ Performance بشكل كبير في القوائم الطويلة. مثلاً، في الجدول اللي كان فيه 1000 صف، الـ Rendering Time نزل من 1.5 ثانية لـ 50 مللي بعد ما طبقنا الـ Virtual Scrolling.
الـ Web Workers بيخليك تشغل الكود الثقيل بعيداً عن الـ Main Thread، ده بيخلي الـ UI سريع وسلس حتى لو عندك عمليات معقدة بتجري في الخلفية. مثلاً، لو عندك عملية بتحسب البيانات أو بتعمل معالجة للصور، الـ Web Worker يقدر يشغلها بعيداً عن الـ Main Thread، وبالتالي الـ UI مش هيعلق. في Angular، تقدر تستخدم الـ @angular/platform-webworker عشان تضيف الـ Web Workers بسهولة.
// main.ts
import { bootstrapWorkerUi } from '@angular/platform-webworker';
bootstrapWorkerUi('worker.bundle.js');
// worker.ts
import { bootstrapWorker } from '@angular/platform-webworker';
bootstrapWorker(AppModule);في الكود ده، الـ Main Thread بيـ Bootstrap الـ Worker، والـ Worker بيـ Bootstrap الـ AppModule. ده بيخلي كل الكود اللي في الـ AppModule يشتغل بعيداً عن الـ Main Thread. لكن احذر: الـ Web Workers مش مناسب لكل حاجة. مثلاً، لو عندك كود بيعتمد على الـ DOM أو الـ Browser APIs، مش هتقدر تشغله في الـ Worker. لازم تتأكد إن الكود اللي هتشغله في الـ Worker مستقل تماماً عن الـ UI.
زي ما شرحنا قبل كده، الـ OnPush Strategy بيقلل عدد الـ Change Detection Checks. لكن عشان الـ OnPush يشتغل بكفاءة، لازم تستخدم الـ Immutable Data. الـ Immutable Data هو البيانات اللي مش بتتغير بعد ما تتخلق، بدل ما تعدل على البيانات الموجودة، بتخلق نسخة جديدة منها. ده بيخلي الـ Change Detection أسرع، خصوصاً في التطبيقات الكبيرة.
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
@Input() users: User[] = [];
addUser(newUser: User) {
this.users = [...this.users, newUser]; // Immutable update
}
}في الكود ده، بدل ما نعدل على الـ users Array مباشرة، بنخلق نسخة جديدة منها باستخدام الـ Spread Operator. ده بيخلي الـ Change Detection أسرع، خصوصاً لو عندك مكونات كتيرة بتستخدم الـ OnPush Strategy. الـ Immutable Data بيقلل فرص الـ Side Effects ويخلي الكود أسهل في الـ Debugging والتحليل.
تحسين الأداء مش بيبدأ بالتعديل على الكود، بيبدأ بالقياس. من غير ما تعرف إيه اللي بيأثر على الأداء، هتضيع وقتك في تحسين حاجات مش هتفرق كتير. مثلاً، في أحد المشاريع اللي اشتغلنا عليها، كان الـ Team بيعتقد إن الـ Network Requests هي سبب البطء، لكن بعد ما حللنا الأداء باستخدام Chrome DevTools، اكتشفنا إن الـ Change Detection هو السبب الرئيسي. بعد ما غيرنا الـ Strategy لـ OnPush، الـ Performance اتحسن بشكل كبير.
أول أداة لازم تستخدمها هي Chrome DevTools. الـ Performance Tab في DevTools بيخليك تقيس الـ Rendering Time، الـ JavaScript Execution Time، والـ Network Requests. تقدر تستخدمه عشان تعرف إيه اللي بيأثر على الأداء وتحدد الأولويات. مثلاً، لو الـ JavaScript Execution Time طويل، ممكن تحتاج تستخدم الـ Web Workers أو تحسن الكود اللي بيعمل الـ Calculations. لو الـ Rendering Time طويل، ممكن تحتاج تستخدم الـ OnPush Strategy أو الـ Virtual Scrolling.
Angular DevTools هو إضافة للمتصفح بتخليك تحلل أداء مكونات Angular بسهولة. تقدر تستخدمه عشان تعرف إيه اللي بيأثر على الـ Change Detection، وتعرف إيه اللي بيستهلك الـ Memory. مثلاً، لو عندك مكون بيـ Check كتير، الـ DevTools هتظهرلك عدد الـ Checks اللي حصلت للمكون ده، وهتساعدك تعرف إيه اللي بيسبب المشكلة.
الـ DevTools كمان بيظهرلك الـ Component Tree، وبيخليك تعرف إيه اللي بيستهلك الـ Memory. مثلاً، لو عندك مكون بيستهلك 50 ميجا من الـ RAM، الـ DevTools هتظهرلك حجم الـ Component في الـ Memory، وهتساعدك تعرف إيه اللي بيستهلك المساحة دي. ده بيخليك تركز على تحسين المكونات اللي عندها مشاكل فعلية، بدل ما تضيع وقتك في تحسين حاجات مش هتفرق كتير.
Lighthouse هو أداة من Google بتقيم أداء التطبيق وتديك نصائح لتحسينه. بتقدر تستخدمه عشان تعرف إيه اللي بيأثر على الـ Performance، الـ Accessibility، والـ SEO. مثلاً، لو الـ Performance Score منخفض، Lighthouse هيديلك قائمة بالحاجات اللي ممكن تحسنها، زي تقليل حجم الـ Images، أو استخدام الـ Lazy Loading، أو تحسين الـ JavaScript Execution Time.
Lighthouse كمان بيقيم الـ Accessibility و الـ SEO، ده بيخليك تتأكد إن تطبيقك متوافق مع معايير الـ Accessibility ومحسّن لمحركات البحث. مثلاً، لو عندك صور مش فيها alt text، Lighthouse هينبهك، وهتساعدك تحسن الـ Accessibility. لو عندك مشاكل في الـ SEO زي الـ Meta Tags الناقصة، Lighthouse هينبهك، وهتساعدك تحسن ترتيب موقعك في نتائج البحث.
بعد ما شرحنا كل التقنيات دي، هديك نصائح عملية تقدر تطبقها فوراً عشان تحسن أداء تطبيقك Angular:
التحسين مش عملية لمرة واحدة، ده عملية مستمرة. كل ما تضيف ميزة جديدة للتطبيق، لازم تقيس الأداء وتأكد إن الميزة الجديدة مش بتأثر على الـ Performance. استخدم الأدوات اللي شرحناها، وحلل الأداء بانتظام، وحسن الكود اللي بيأثر على الـ Performance. ده اللي هيخليك تبني تطبيق Angular سريع وسلس، ويخلي المستخدمين سعداء بتجربة الاستخدام.
في النهاية، تحسين أداء Angular مش بس عن تحسين الكود، ده عن فهم إزاي الـ Framework بيشتغل خلف الكواليس، وإزاي تقدر تستغل الأدوات اللي عندك أحسن استغلال. كل تقنية شرحناها في المقال ده مجربة في الإنتاج، وكلها بتفرق بشكل كبير في الأداء. ابدأ بقياس الأداء، حدد الأولويات، وطبق الحلول اللي تناسب تطبيقك. النتيجة؟ تطبيق سريع وسلس، ومستخدمين سعداء.