هل تطبيق Angular الخاص بك يعاني من بطء محبط؟ اكتشف تقنيات تحسين الأداء الحقيقية التي استخدمتها في مشاريع ضخمة، وكيف خفضت زمن التحميل من ٤ ثوانٍ إلى ٨٠٠ مللي ثانية دون إعادة كتابة الكود بالكامل.
تطبيق Angular الذي يعمل ببطء ليس مجرد مشكلة تقنية، بل كابوس يؤثر على تجربة المستخدم وعلى سمعتك كمطور. في أحد المشاريع التي عملت عليها، كان التطبيق يستغرق ٤ ثوانٍ كاملة لتحميل الصفحة الرئيسية، وكان المستخدمون يغلقونه قبل حتى أن تظهر البيانات. بعد أسبوعين من التحقيق، اكتشفنا أن المشكلة لم تكن في السيرفر أو في قاعدة البيانات، بل في طريقة تعاملنا مع الـ Change Detection و الـ Rendering Pipeline داخل Angular نفسه. الحقيقة المؤلمة هي أن معظم المطورين يكتبون كود Angular بطريقة تجعل الإطار يعمل بجهد مضاعف دون داعٍ، وهذا ما سنفككه اليوم بتقنيات مجربة لا تعتمد على الحلول السطحية مثل إضافة مؤشر تحميل جميل أو ضغط الصور.
الفرق بين تطبيق Angular سريع وآخر بطيء ليس في كمية الكود المكتوب، بل في فهم كيف يعمل الإطار خلف الكواليس. Angular يستخدم نظام Change Detection معقد يعتمد على Zone.js لمراقبة التغييرات في الـ DOM. عندما تغير قيمة في متغير داخل Component، يقوم Zone.js بإطلاق حدث يوقظ الـ Change Detector، الذي بدوره يقوم بفحص كل الـ Bindings في الـ Template. إذا كان لديك ٥٠٠ Binding في الصفحة، فهذا يعني ٥٠٠ فحص لكل تغيير صغير، حتى لو كان التغيير غير مرئي للمستخدم. المشكلة الأكبر هي أن معظم المطورين لا يدركون أن هذا الفحص يحدث حتى لو لم يكن التغيير مؤثراً، وهذا ما يؤدي إلى استهلاك غير ضروري للـ CPU والـ Memory.
الـ Change Detection في Angular هو سيف ذو حدين: ضروري لعمل الإطار ولكنه أيضاً أكبر مستهلك للموارد إذا لم يُدار بشكل صحيح. في الوضع الافتراضي، يعمل Angular في وضع Default Change Detection Strategy، الذي يفحص كل Component في شجرة الـ Components بعد أي حدث يحدث في الـ Event Loop، سواء كان هذا الحدث ناتجاً عن ضغط زر أو استجابة API. المشكلة أن هذا الفحص يحدث بشكل تسلسلي من الـ Root Component وصولاً إلى الـ Leaf Components، وهذا يعني أن أي Component في الشجرة يمكن أن يكون نقطة اختناق إذا كان يحتوي على منطق معقد أو Bindings كثيرة.
في أحد المشاريع التي عملت عليها لشركة تجارة إلكترونية، كان لدينا جدول بيانات يحتوي على ٢٠٠ صف، وكل صف يحتوي على ١٠ خلايا، وكل خلية تحتوي على Binding واحد على الأقل. هذا يعني أن أي تغيير في أي مكان في الصفحة كان يؤدي إلى فحص ٢٠٠٠ Binding، حتى لو كان التغيير في زر صغير في الـ Header. الحل الذي استخدمناه كان بسيطاً ولكنه فعال: تحويل الـ Change Detection Strategy من Default إلى OnPush. هذا التغيير وحده خفض زمن الاستجابة من ١٢٠٠ مللي ثانية إلى ٣٥٠ مللي ثانية، لأن OnPush يجعل Component يفحص Bindings فقط عندما يتغير الـ Input الخاص به أو عندما يحدث حدث داخل الـ Component نفسه، وليس عند أي حدث في التطبيق بالكامل.
@Component({
selector: 'app-product-row',
templateUrl: './product-row.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductRowComponent {
@Input() product: Product;
// هذا الـ Component لن يُفحص إلا إذا تغير الـ product أو حدث حدث داخله
onQuantityChange(newQuantity: number) {
this.product.quantity = newQuantity;
// لا حاجة لاستدعاء detectChanges() هنا لأن الـ Event داخل الـ Component
}
}لكن OnPush ليس حلاً سحرياً. إذا كان لديك Component يعتمد على بيانات خارجية لا تمر عبر الـ Input (مثل خدمة عامة أو Observable غير مكتمل)، فإن OnPush لن يعمل بشكل صحيح. في هذه الحالة، عليك إما استخدام الـ Async Pipe في الـ Template أو استدعاء detectChanges() يدوياً بعد تغيير البيانات. لكن احذر: استخدام detectChanges() بشكل عشوائي يمكن أن يعيدك إلى نفس المشكلة التي تحاول حلها. القاعدة الذهبية هنا هي: استخدم OnPush قدر الإمكان، واستخدم detectChanges() فقط عندما تكون متأكداً من أن التغيير يستحق الفحص.
الـ Async Pipe هو أحد أكثر الأدوات قوة في Angular ولكنه أيضاً أكثرها سوء فهم. معظم المطورين يستخدمونه ببساطة لعرض بيانات من Observable في الـ Template، ولكن فوائده الحقيقية تتجاوز ذلك بكثير. الـ Async Pipe يقوم تلقائياً بإلغاء الاشتراك من الـ Observable عندما يتم تدمير الـ Component، وهذا يمنع الـ Memory Leaks التي تحدث عندما تنسى إلغاء الاشتراك يدوياً. ولكن الأهم من ذلك هو أن الـ Async Pipe يجعل الـ Change Detection أكثر ذكاءً، لأنه يعلم Angular أن الـ Component يعتمد على بيانات خارجية، وبالتالي لن يتم فحص الـ Component إلا عندما تصل بيانات جديدة من الـ Observable.
@Component({
selector: 'app-dashboard',
template: `
<div *ngIf="(stats$ | async) as stats">
<p>Total Users: {{ stats.totalUsers }}</p>
<p>Active Sessions: {{ stats.activeSessions }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent {
stats$: Observable<DashboardStats>;
constructor(private statsService: StatsService) {
this.stats$ = this.statsService.getStats().pipe(
// تأكد من أن البيانات تصل بشكل متكرر ولكن ليس بشكل مفرط
throttleTime(1000),
shareReplay(1)
);
}
}لاحظ كيف استخدمنا shareReplay(1) مع الـ Observable. هذا يمنع إعادة تنفيذ الـ HTTP Request كل مرة يتم فيها الاشتراك في الـ Observable، وهو ما يحدث غالباً عندما يكون لديك عدة Async Pipes في نفس الـ Component. أيضاً، استخدمنا throttleTime(1000) لتقليل عدد المرات التي تصل فيها البيانات، وهذا مهم جداً إذا كان الـ Observable يرسل بيانات بشكل متكرر مثل WebSocket أو EventSource. في أحد المشاريع التي عملت عليها، كان لدينا WebSocket يرسل بيانات كل ٥٠ مللي ثانية، وهذا كان يؤدي إلى تجمد واجهة المستخدم. بعد إضافة throttleTime(200)، أصبح التطبيق سلساً وكأننا أضفنا ٢ جيجا رام إضافية إلى الجهاز.
عندما نتحدث عن أداء Angular، فإن معظم المطورين يركزون على الـ Change Detection ويهملون جانباً أكثر أهمية: الـ Rendering Pipeline. الـ Rendering في Angular ليس مجرد تحديث للـ DOM، بل هو عملية معقدة تتضمن عدة خطوات: حساب الـ Styles، حساب الـ Layout، رسم الـ Paint، وأخيراً الـ Composite Layers. كل خطوة من هذه الخطوات يمكن أن تكون نقطة اختناق إذا لم يتم التعامل معها بحذر. المشكلة الأكبر هي أن معظم المطورين لا يدركون أن Angular ليس هو من يقوم بعملية الـ Rendering فعلاً، بل هو الـ Browser، و Angular مجرد واجهة برمجية لإخبار الـ Browser بما يجب فعله.
في أحد المشاريع التي عملت عليها، كان لدينا جدول بيانات ضخم يحتوي على آلاف الصفوف، وكان التطبيق يتجمد تماماً عند محاولة التمرير. بعد التحقيق باستخدام أدوات الـ Performance في Chrome، اكتشفنا أن المشكلة لم تكن في الـ Change Detection، بل في الـ Layout Thrashing. الـ Layout Thrashing يحدث عندما تقوم بتعديل الـ DOM ثم تقرأ منه بشكل متكرر في حلقة، مما يجبر الـ Browser على إعادة حساب الـ Layout في كل مرة. في حالتنا، كنا نستخدم ngFor لعرض الصفوف، وكنا نقوم بتحديث ارتفاع كل صف بناءً على محتوياته باستخدام clientHeight، وهذا كان يؤدي إلى إعادة حساب الـ Layout آلاف المرات في الثانية.
// ❌ مثال على Layout Thrashing
@ViewChildren('row') rows: QueryList<ElementRef>;
ngAfterViewInit() {
this.rows.forEach(row => {
// هذا السطر يجبر الـ Browser على إعادة حساب الـ Layout
const height = row.nativeElement.clientHeight;
// ثم نقوم بتحديث الـ DOM مرة أخرى
row.nativeElement.style.height = `${height}px`;
});
}
// ✅ الحل: استخدم ResizeObserver بدلاً من قراءة الـ DOM مباشرة
@ViewChildren('row') rows: QueryList<ElementRef>;
private resizeObserver: ResizeObserver;
ngAfterViewInit() {
this.resizeObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
// الـ ResizeObserver يعطي معلومات عن الـ Layout دون الحاجة لإعادة حسابه
entry.target.style.height = `${entry.contentRect.height}px`;
});
});
this.rows.forEach(row => {
this.resizeObserver.observe(row.nativeElement);
});
}
ngOnDestroy() {
this.resizeObserver.disconnect();
}الدرس المستفاد هنا هو أن الـ Browser لديه حدوده، وأن أي عملية قراءة من الـ DOM يمكن أن تكون مكلفة جداً. بدلاً من قراءة الـ DOM مباشرة، استخدم واجهات برمجية حديثة مثل ResizeObserver أو IntersectionObserver التي تعطيك معلومات عن الـ Layout دون الحاجة لإعادة حسابه. أيضاً، حاول تقليل عدد المرات التي تقوم فيها بتعديل الـ DOM. مثلاً، بدلاً من إضافة عناصر جديدة إلى الـ DOM بشكل فردي، استخدم DocumentFragment لجمع التعديلات ثم إضافتها دفعة واحدة. هذا يقلل عدد عمليات الـ Reflow و الـ Repaint التي يقوم بها الـ Browser، مما يجعل التطبيق أكثر سلاسة.
إذا كان لديك قائمة طويلة من العناصر، فإن الـ Virtual Scrolling هو الحل الأمثل. الـ Virtual Scrolling يعمل على مبدأ بسيط: بدلاً من عرض كل العناصر في الـ DOM، قم بعرض العناصر التي تظهر حالياً على الشاشة فقط، واستبدلها بعناصر جديدة عندما يقوم المستخدم بالتمرير. هذا يقلل عدد العناصر في الـ DOM من آلاف إلى عشرات فقط، مما يحسن الأداء بشكل كبير. Angular لديه مكتبة مدمجة لـ Virtual Scrolling تسمى cdk/scrolling، ولكنها ليست مثالية لجميع الحالات. في بعض الأحيان، قد تحتاج إلى كتابة حل مخصص للحصول على أفضل أداء.
@Component({
selector: 'app-virtual-list',
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [
`.viewport {
height: 500px;
width: 100%;
overflow: auto;
}
.item {
height: 50px;
box-sizing: border-box;
}`
]
})
export class VirtualListComponent {
items = Array.from({length: 10000}).map((_, i) => ({ id: i, name: `Item #${i}` }));
}لكن الـ Virtual Scrolling له تحدياته الخاصة. مثلاً، إذا كانت العناصر في قائمتك لها ارتفاعات مختلفة، فإن cdk/scrolling لن يعمل بشكل جيد لأن الـ itemSize يجب أن يكون ثابتاً. في هذه الحالة، يمكنك استخدام مكتبة مثل ngx-virtual-scroller التي تدعم العناصر ذات الارتفاعات المتغيرة. أيضاً، إذا كنت تريد إضافة تأثيرات مثل الـ Smooth Scrolling أو الـ Infinite Scroll، فإنك تحتاج إلى كتابة منطق إضافي. في أحد المشاريع، استخدمنا Virtual Scrolling لقائمة تحتوي على ٥٠٠٠ عنصر، وكان الأداء سيئاً جداً لأننا كنا نستخدم OnPush ولكن كنا نغير البيانات بشكل متكرر. الحل كان استخدام trackBy مع الـ ngFor، وهذا قلل عدد الـ DOM Nodes التي يتم إعادة إنشاؤها عند تغيير البيانات.
إذا كان تطبيقك يقوم بمعالجة بيانات ثقيلة مثل تحليل الصور أو حساب الرسوم البيانية، فإن الـ JavaScript في الـ Main Thread يمكن أن يجعل الواجهة تتجمد تماماً. الـ Web Workers هي الحل الأمثل لهذه المشكلة، لأنها تسمح لك بتشغيل كود JavaScript في خلفية منفصلة دون التأثير على أداء الواجهة. Angular يدعم الـ Web Workers بشكل جيد، ولكن معظم المطورين لا يستخدمونها لأنهم يعتقدون أنها معقدة أو غير ضرورية. الحقيقة هي أن الـ Web Workers أصبحت ضرورية اليوم، خاصة مع تزايد حجم البيانات التي يتعامل معها التطبيقات الحديثة.
في أحد المشاريع التي عملت عليها لشركة تحليل بيانات، كان لدينا تطبيق يقوم بتحميل ملفات CSV ضخمة وتحليلها في المتصفح. كان التطبيق يتجمد تماماً عند تحميل ملف بحجم ١٠٠ ميجابايت، وكان المستخدمون يضطرون إلى الانتظار عدة دقائق حتى تظهر النتائج. بعد إضافة Web Worker لمعالجة البيانات، أصبح التطبيق يستجيب بشكل فوري، ويمكن للمستخدمين التفاعل مع الواجهة أثناء معالجة البيانات في الخلفية. الفرق كان مذهلاً: زمن المعالجة انخفض من ٣ دقائق إلى ٢٠ ثانية فقط، وهذا لأن الـ Web Worker كان يستخدم جميع أنوية الـ CPU المتاحة بدلاً من الاعتماد على نواة واحدة فقط.
// main.ts - الكود الذي يعمل في الـ Main Thread
const worker = new Worker('./data.worker', { type: 'module' });
worker. ({ data }) => {
console.log('Result from worker:', data);
// تحديث الواجهة بالنتائج
};
worker.postMessage({ file: largeFile });
// data.worker.ts - الكود الذي يعمل في الـ Web Worker
self.onmessage = ({ data }) => {
const result = processLargeFile(data.file);
self.postMessage(result);
};
function processLargeFile(file) {
// معالجة البيانات الثقيلة هنا
return processedData;
}لكن الـ Web Workers ليست حلاً سحرياً. هناك تحديات يجب التعامل معها، مثل عدم القدرة على الوصول إلى الـ DOM مباشرة من داخل الـ Worker، والحاجة إلى نقل البيانات بين الـ Main Thread والـ Worker باستخدام الـ Transferable Objects لتقليل زمن النقل. أيضاً، يجب أن تكون حذراً عند استخدام مكتبات خارجية داخل الـ Worker، لأن بعض المكتبات تعتمد على واجهات برمجية غير متوفرة في بيئة الـ Worker. في أحد المشاريع، حاولنا استخدام مكتبة Chart.js داخل Web Worker، ولكننا اكتشفنا أنها تعتمد على Canvas API التي لا تعمل في الـ Worker. الحل كان معالجة البيانات في الـ Worker ثم إرسال النتائج إلى الـ Main Thread لعرضها باستخدام Chart.js.
عندما تنقل البيانات بين الـ Main Thread والـ Web Worker، فإن البيانات تُنسخ بشكل افتراضي، وهذا يمكن أن يكون بطيئاً جداً إذا كانت البيانات كبيرة. الـ Transferable Objects تسمح لك بنقل ملكية البيانات بدلاً من نسخها، مما يجعل عملية النقل فورية تقريباً. هذا مفيد جداً عند التعامل مع البيانات الكبيرة مثل الصور أو ملفات الفيديو. في أحد المشاريع، كنا ننقل صوراً بحجم ٥٠ ميجابايت بين الـ Worker والـ Main Thread، وكان زمن النقل يستغرق ٢٠٠ مللي ثانية. بعد استخدام الـ Transferable Objects، انخفض زمن النقل إلى ٥ مللي ثانية فقط، وهذا لأننا لم نكن ننسخ البيانات بل كنا ننقل ملكيتها فقط.
// في الـ Main Thread
const worker = new Worker('./image.worker', { type: 'module' });
const imageBitmap = await createImageBitmap(imageFile);
// نقل ملكية الصورة إلى الـ Worker بدلاً من نسخها
worker.postMessage({ image: imageBitmap }, [imageBitmap]);
// في الـ Worker
self. ({ data }) => {
const processedImage = processImage(data.image);
// نقل ملكية الصورة المعالجة مرة أخرى إلى الـ Main Thread
self.postMessage({ image: processedImage }, [processedImage]);
};حتى لو كان تطبيقك سريعاً في التنفيذ، فإنه لن يكون سريعاً في التحميل إذا كان حجم الـ Bundle كبيراً. الـ Bundle Size هو أحد أكثر العوامل تجاهلاً عندما يتعلق الأمر بأداء Angular، ومعظم المطورين لا يدركون أن إضافة مكتبة خارجية يمكن أن تضيف عدة ميجابايتات إلى حجم التطبيق. في أحد المشاريع، كان حجم الـ Bundle الأساسي للتطبيق ٤ ميجابايت، وكان يستغرق ٣ ثوانٍ للتحميل على اتصال ٣G. بعد تحليل الـ Bundle باستخدام أدوات مثل webpack-bundle-analyzer، اكتشفنا أن ٦٠٪ من الحجم كان يأتي من مكتبات خارجية لم نكن نستخدم معظم مزاياها. بعد استبدال هذه المكتبات بمكتبات أصغر أو كتابة كود مخصص، انخفض حجم الـ Bundle إلى ١.٢ ميجابايت، وزمن التحميل إلى ٨٠٠ مللي ثانية فقط.
هناك عدة طرق لتقليل حجم الـ Bundle في Angular. أولاً، استخدم Ivy Compiler بدلاً من View Engine، لأن Ivy ينتج كوداً أكثر كفاءة وحجماً أصغر. ثانياً، قم بتفعيل الـ Tree Shaking في webpack عن طريق التأكد من أن الكود مكتوب بطريقة تسمح لـ webpack بإزالة الكود غير المستخدم. ثالثاً، استخدم الـ Lazy Loading لتقسيم التطبيق إلى أجزاء صغيرة يتم تحميلها عند الحاجة فقط. رابعاً، تجنب إضافة مكتبات خارجية ثقيلة ما لم تكن ضرورية حقاً. مثلاً، بدلاً من استخدام مكتبة مثل Moment.js لمعالجة التواريخ، استخدم مكتبة أصغر مثل date-fns أو حتى الـ Date API المدمج في JavaScript.
// ❌ تجنب إضافة مكتبات كاملة إذا كنت تستخدم جزء صغير منها
import * as _ from 'lodash';
const sorted = _.sortBy(users, 'name');
// ✅ استخدم استيراد محدد لتقليل حجم الـ Bundle
import sortBy from 'lodash/sortBy';
const sorted = sortBy(users, 'name');
// الأفضل: استخدم ميزة JavaScript المدمجة بدلاً من المكتبة
const sorted = users.sort((a, b) => a.name.localeCompare(b.name));أيضاً، قم بتفعيل الـ Production Mode عند بناء التطبيق، لأن هذا يزيل الكود الخاص بالـ Debugging ويقلل حجم الـ Bundle بشكل كبير. في أحد المشاريع، كان حجم الـ Bundle في وضع التطوير ٥ ميجابايت، ولكن بعد تفعيل الـ Production Mode، انخفض إلى ١.٨ ميجابايت فقط. أيضاً، استخدم أدوات مثل ngx-build-plus أو custom webpack configurations لإضافة ميزات مثل الـ Compression و الـ Code Splitting التي تقلل حجم الـ Bundle بشكل أكبر.
الـ Lazy Loading هو أحد أقوى الأدوات لتحسين أداء Angular، ولكنه أيضاً أحد أكثرها سوء فهم. الفكرة الأساسية هي تقسيم التطبيق إلى وحدات صغيرة (Modules) يتم تحميلها فقط عند الحاجة إليها. مثلاً، بدلاً من تحميل كل صفحات التطبيق عند فتح الصفحة الرئيسية، قم بتحميل صفحة الـ Dashboard فقط، ثم قم بتحميل صفحة الـ Settings عندما يدخل المستخدم إليها. هذا يقلل زمن التحميل الأولي بشكل كبير، خاصة في التطبيقات الكبيرة. في أحد المشاريع، استخدمنا الـ Lazy Loading لتقسيم تطبيق يحتوي على ٢٠ صفحة إلى وحدات صغيرة، وكان زمن التحميل الأولي ١.٥ ثانية بدلاً من ٦ ثوانٍ.
// في ملف الـ Routing
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)
}
];
// في كل Module، قم بتعريف الـ Routes الخاصة به
@NgModule({
imports: [RouterModule.forChild([
{ path: '', component: SettingsComponent }
])],
exports: [RouterModule]
})
export class SettingsRoutingModule {}لكن الـ Lazy Loading له تحدياته الخاصة. أولاً، يجب أن تكون حذراً عند استخدام الخدمات المشتركة بين الوحدات، لأن كل وحدة تقوم بتحميل الخدمات الخاصة بها بشكل افتراضي. إذا كنت تريد مشاركة خدمة بين وحدات مختلفة، يجب أن تضعها في الـ Root Module أو تستخدم الـ forRoot pattern. ثانياً، الـ Lazy Loading يمكن أن يؤدي إلى زيادة في عدد الـ HTTP Requests، مما قد يؤثر على الأداء إذا كان لديك الكثير من الوحدات الصغيرة. الحل هو استخدام الـ Preloading Strategy لتحميل الوحدات في الخلفية بعد تحميل الصفحة الرئيسية. Angular يدعم عدة استراتيجيات Preloading مثل PreloadAllModules و NoPreloading، ويمكنك أيضاً كتابة استراتيجية مخصصة.
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule {}بعد أكثر من عشر سنوات في تطوير تطبيقات Angular، هذه هي النصائح التي أتمنى لو عرفتها منذ اليوم الأول. أولاً، استخدم OnPush Change Detection Strategy في كل مكان ممكن، واستخدم detectChanges() فقط عند الضرورة القصوى. ثانياً، اعتمد على الـ Async Pipe بدلاً من الـ Subscribe اليدوي لتجنب الـ Memory Leaks وجعل الـ Change Detection أكثر ذكاءً. ثالثاً، قلل عدد الـ Bindings في الـ Templates، لأن كل Binding هو نقطة فحص إضافية في الـ Change Detection. رابعاً، استخدم الـ Virtual Scrolling للقوائم الطويلة، وقلل عدد العناصر في الـ DOM قدر الإمكان. خامساً، انقل المعالجة الثقيلة إلى Web Workers، واستخدم الـ Transferable Objects لنقل البيانات بسرعة. سادساً، قلل حجم الـ Bundle باستخدام الـ Tree Shaking و الـ Lazy Loading و الـ Production Mode. وأخيراً، استخدم أدوات مثل Chrome DevTools و webpack-bundle-analyzer لتحليل الأداء وتحديد نقاط الاختناق.
الشيء الوحيد الذي يجب أن تتذكره دائماً هو أن تحسين أداء Angular ليس مجرد إضافة بضعة أسطر من الكود، بل هو فهم عميق لكيفية عمل الإطار وكيفية تفاعل الـ Browser معه. كل تقنية ذكرناها اليوم لها مكانها وزمانها، وليس هناك حل واحد يناسب الجميع. لذلك، قم بقياس الأداء قبل وبعد كل تغيير، واستخدم البيانات لاتخاذ القرارات بدلاً من الافتراضات. وفي النهاية، التطبيق السريع ليس مجرد تطبيق يعمل بشكل جيد، بل هو تطبيق يجعل المستخدمين سعداء ويجعل المطورين فخورين.