في عالم يتسارع فيه إطار عمل جديد كل ستة أشهر، يظل Angular يقف كعملاق صلب لكنه بطيء في التكيف. هل لا يزال مناسباً للمشاريع الجديدة في 2025 أم أنه تحول إلى عبء تقني؟ تحليل عميق من مهندس واجه Angular في مشاريع ضخمة وحقق نتائج مذهلة — وأحياناً كارثية.
في أحد أيام 2023، كنت جالساً في مكتب شركة ناشئة في دبي، والشاشة أمامي تظهر لوحة تحكم معقدة مبنية بـ Angular. الصفحة تستغرق 4.7 ثانية لتحميل بالكامل على شبكة 4G، بينما نسخة تجريبية مبنية بـ React تحمل في 1.2 ثانية. العميل يصرخ: "لماذا نحتاج إلى 600 كيلوبايت من جافا سكريبت فقط لصفحة تسجيل دخول؟" الحقيقة المؤلمة هي أن Angular لم يكن مصمماً للسرعة الخارقة، بل للصلابة والتنظيم. لكن في 2025، هل الصلابة كافية عندما يتوقع المستخدمون تجربة فورية؟
الـ Bundle Size في Angular ليس مجرد رقم في تقرير Lighthouse — إنه مشكلة حقيقية تؤثر على معدلات التحويل. في مشروع عملت عليه العام الماضي، قلصنا حجم الحزمة من 1.2 ميجابايت إلى 450 كيلوبايت عن طريق التحويل إلى Standalone Components وإزالة المكتبات الزائدة. النتيجة؟ زيادة بنسبة 22% في معدلات التحويل على الهواتف ذات الاتصال الضعيف. لكن السؤال يبقى: لماذا نحتاج إلى كل هذا الجهد لتحسين شيء كان يجب أن يكون مثالياً منذ البداية؟
آلية الـ Change Detection في Angular هي واحدة من أكثر الميزات فهمًا بشكل خاطئ. في الأساس، هي نظام ذكي يتتبع التغيرات في البيانات ويعكسها على واجهة المستخدم. لكن خلف الكواليس، يحدث شيء أكثر تعقيداً: Angular ينشئ شجرة كاملة من الـ Change Detectors، وكل مكون له كائن خاص به. عندما يتغير أي شيء، سواء كان ذلك نقرة زر أو تحديث لقيمة في input، Angular يقوم بتشغيل دورة كاملة من الـ Change Detection، بدءاً من الجذر وصولاً إلى كل مكون فرعي.
المشكلة تظهر عندما يكون لديك تطبيق معقد مع مئات المكونات. في أحد المشاريع التي عملت عليها لشركة سعودية، كان لدينا جدول بيانات يحتوي على 500 صف، وكل صف يحتوي على 10 مكونات فرعية. عند تحديث قيمة واحدة، كانت Angular تشغل 5000 دورة تغيير — حتى مع استخدام OnPush. الحل؟ اضطررنا لإعادة هيكلة التطبيق بالكامل باستخدام الـ Zone.js بشكل يدوي وتطبيق استراتيجيات مثل الـ Detach Change Detector. لكن هذا ليس حلاً مثالياً — إنه مجرد لاصق على جرح غائر.
// مثال على استخدام ChangeDetectionStrategy.OnPush لتقليل عدد دورات التغيير
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-product-row',
template: `{{ product.name }} - {{ product.price | currency }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductRowComponent {
@Input() product: any;
// مع OnPush، لن يتم تشغيل التغيير إلا إذا تغير المرجع الكامل للكائن
// وليس مجرد تغيير قيمة داخله
}
// في المكون الأب، يجب تمرير كائن جديد عند التحديث
this.products = [...this.products]; // بدلاً من this.products[0].price = 100;الـ OnPush ليس حلاً سحرياً — إنه مجرد أداة تتطلب فهماً عميقاً لكيفية عمل المراجع في جافا سكريبت. في كثير من الأحيان، يجد المطورون أنفسهم يقاتلون النظام بدلاً من الاستفادة منه. مثلاً، عند استخدام مكتبات خارجية مثل RxJS، قد لا تعمل OnPush كما هو متوقع لأن الـ Observables لا تغير المرجع الأساسي للكائن. الحل؟ استخدام الـ async pipe في القوالب، لكن هذا يضيف طبقة أخرى من التعقيد.
عندما أطلق Angular Ivy في الإصدار 8، وعدت جوجل بثورة في الأداء والحجم. والحقيقة هي أن Ivy حقق تحسناً ملحوظاً، لكنه لم يكن كافياً. الـ Tree Shaking أصبح أكثر فعالية، وحجم التطبيقات قل بنسبة 30% في المتوسط. لكن المشكلة الأكبر — الـ Runtime Overhead — لم تحل بالكامل. حتى مع Ivy، لا يزال Angular يضيف عشرات الكيلوبايتات من الكود فقط لإدارة النظام الداخلي مثل الـ Dependency Injection والـ Change Detection.
في مشروع حديث، قارنا بين تطبيق مبني بـ Angular وآخر مبني بـ Svelte. نفس الوظائف بالضبط، نفس التصميم، لكن الفرق في الأداء كان صارخاً. التطبيق المبني بـ Svelte حمل في 300 ميللي ثانية واستخدم 150 كيلوبايت من جافا سكريبت، بينما تطبيق Angular استغرق 1.1 ثانية واستخدم 550 كيلوبايت. الأرقام لا تكذب — Ivy حسن الأمور، لكنه لم يجعل Angular خفيفاً.
# مقارنة حجم الحزمة بين Angular و Svelte
# نفس التطبيق البسيط: صفحة تسجيل دخول + قائمة منتجات
# Angular (مع Ivy)
ng build --prod
# Output: dist/main.[hash].js - 548 KB
# Svelte
npm run build
# Output: public/build/bundle.js - 147 KBنظام الـ Dependency Injection في Angular هو واحد من أقوى ميزاته، لكنه أيضاً أحد أكبر أسباب التعقيد. في التطبيقات الكبيرة، يصبح من الصعب تتبع تدفق البيانات والخدمات، خاصة عندما تستخدم خدمات مشتركة بين عشرات المكونات. في أحد المشاريع التي عملت عليها، كان لدينا خدمة تسمى UserService تُحقن في 47 مكون مختلف. عندما أردنا إضافة ميزة جديدة، اضطررنا لتحديث كل المكونات التي تستخدم هذه الخدمة — حتى تلك التي لا تحتاج للميزة الجديدة.
الحل الذي وجدناه هو تقسيم الخدمات إلى وحدات أصغر واستخدام الـ Injection Tokens لتحديد نطاق كل خدمة. لكن هذا يتطلب تخطيطاً مسبقاً دقيقاً، وهو شيء لا يتوفر دائماً في المشاريع السريعة. في الواقع، معظم المطورين لا يفكرون في هذا الأمر حتى يصبح التطبيق بطيئاً أو يصعب صيانته. وعندما يصلون لهذه المرحلة، يكون الأوان قد فات لإعادة هيكلة كل شيء.
// تقسيم الخدمات باستخدام Injection Tokens
import { InjectionToken } from '@angular/core';
// بدلاً من خدمة واحدة ضخمة
// export class UserService { ... }
// نقسمها إلى خدمات أصغر
const USER_CACHE = new InjectionToken<UserCacheService>('UserCache');
const USER_API = new InjectionToken<UserApiService>('UserApi');
// في الوحدة
@NgModule({
providers: [
{ provide: USER_CACHE, useClass: UserCacheService },
{ provide: USER_API, useClass: UserApiService }
]
})
export class UserModule {}
// في المكون
constructor(@Inject(USER_API) private userApi: UserApiService) {}إدارة الحالة في Angular هي واحدة من أكثر الجوانب إحباطاً. بينما توفر المكتبات مثل NgRx حلولاً قوية، إلا أنها تأتي بثمن باهظ: التعقيد. في أحد المشاريع، اضطررنا لتنفيذ NgRx لإدارة حالة معقدة تتضمن أكثر من 50 إجراء مختلف. النتيجة؟ كود يصعب قراءته وفهمه، حتى بالنسبة للمطورين الذين كتبوه. وعندما أردنا إضافة ميزة جديدة، استغرق الأمر أسبوعين فقط لفهم تدفق البيانات الحالي.
الحقيقة هي أن معظم التطبيقات لا تحتاج إلى NgRx. في كثير من الأحيان، يكفي استخدام خدمات بسيطة مع RxJS لإدارة الحالة. لكن المشكلة هي أن Angular يشجع على استخدام الحلول المعقدة من البداية. مثلاً، عند إنشاء مشروع جديد باستخدام Angular CLI، لا يوجد خيار لاختيار نظام إدارة حالة بسيط — عليك إما استخدام الخدمات العادية أو القفز مباشرة إلى NgRx أو Akita.
// إدارة حالة بسيطة باستخدام RxJS بدون NgRx
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CartService {
private cartItems = new BehaviorSubject<Product[]>([]);
cartItems$ = this.cartItems.asObservable();
addToCart(product: Product) {
this.cartItems.next([...this.cartItems.value, product]);
}
removeFromCart(productId: string) {
this.cartItems.next(
this.cartItems.value.filter(item => item.id !== productId)
);
}
}
// في المكون
this.cartService.cartItems$.subscribe(items => {
this.cartItems = items;
});خلال السنوات العشر الماضية، استخدمت Angular في مشاريع متنوعة: من تطبيقات إدارة المستشفيات إلى منصات التجارة الإلكترونية. التجربة لم تكن دائماً سيئة — في الواقع، في المشاريع الكبيرة التي تتطلب هيكلية قوية، كان Angular خياراً ممتازاً. مثلاً، في مشروع لشركة طيران كبيرة، استخدمنا Angular لبناء لوحة تحكم معقدة لإدارة الرحلات. النظام كان مستقراً وسهل الصيانة بفضل النظام القوي للـ Modules والـ Dependency Injection.
لكن في المشاريع الصغيرة أو المتوسطة، كانت القصة مختلفة. في شركة ناشئة في القاهرة، استخدمنا Angular لبناء تطبيق توصيل طعام. بعد ستة أشهر، وجدنا أنفسنا نقاتل مع حجم الحزمة وأدوات البناء المعقدة. عندما قررنا التحويل إلى React، قلصنا وقت التحميل من 3.5 ثانية إلى 1.1 ثانية، وزادت معدلات التحويل بنسبة 35%. الأرقام كانت واضحة — Angular ليس الخيار الأمثل لكل مشروع.
إذا كنت تفكر في استخدام Angular لمشروع جديد في 2025، اسأل نفسك هذه الأسئلة الثلاثة: هل فريقك متمرس في TypeScript و RxJS؟ هل مشروعك يتطلب هيكلية قوية وصيانة طويلة الأمد؟ هل مستعد لقبول بعض البطء في الأداء مقابل استقرار النظام؟ إذا كانت الإجابة نعم على كل هذه الأسئلة، فقد يكون Angular خياراً جيداً. لكن إذا كنت تبحث عن سرعة تطوير عالية أو تجربة مستخدم فائقة، فربما يجب عليك النظر في بدائل مثل React أو Svelte.
القرار النهائي؟ Angular ليس ميتاً، لكنه أيضاً ليس الخيار الأمثل لكل مشروع. إنه أداة قوية لكنها ثقيلة — مثل استخدام شاحنة لنقل بضعة صناديق. إذا كنت بحاجة إلى الشاحنة، فهي رائعة. لكن إذا كنت تريد سرعة وخفة، فربما تحتاج إلى سيارة صغيرة.