تطبيق Angular بطيء؟ اكتشف التقنيات الحقيقية التي تستخدمها الشركات الكبرى لتحويل تطبيقاتها من بطيئة إلى سريعة، مع شرح عميق لما يحدث خلف الكواليس في الذاكرة والمعالج.
عندما يفتح المستخدم تطبيق Angular الخاص بك ويتجمد المتصفح لثلاث ثوانٍ قبل أن يظهر أي شيء، فالمشكلة ليست مجرد بطء — إنها كارثة تجربة مستخدم. في عام ٢٠٢٣، وجدت دراسة أجرتها Google أن ٥٣٪ من المستخدمين يغادرون صفحة ويب إذا استغرق تحميلها أكثر من ثلاث ثوانٍ. لكن المشكلة الحقيقية ليست في الثواني نفسها، بل في ما يحدث خلف الكواليس: تطبيقات Angular الثقيلة ترسل ميجابايتات من JavaScript للمتصفح، وتستهلك الذاكرة بشكل مفرط، وتسبب تجميد الـ Event Loop. الحقيقة هي أن معظم المطورين لا يعرفون كيف يعمل Angular تحت الغطاء، وبالتالي لا يعرفون كيف يصلحون الأداء.
في هذا المقال، لن نتحدث عن النصائح العامة مثل "استخدم OnPush" أو "قلل من حجم الكود". بدلاً من ذلك، سنغوص في تقنيات مجربة تستخدمها شركات مثل Microsoft وAdobe لتحسين أداء تطبيقات Angular الخاصة بها. سنشرح ماذا يحدث في الذاكرة والمعالج عندما يعمل التطبيق، وكيف يمكنك تجنب الفخاخ الشائعة التي يقع فيها حتى المطورون ذوو الخبرة. كل تقنية هنا مدعومة بأكواد حقيقية واختبارات أداء، وليس مجرد نظريات.
أول خطأ يقع فيه المطورون هو الاعتقاد بأن Angular بطيء بطبيعته. الحقيقة هي أن Angular مصمم ليكون سريعاً، لكن سوء الاستخدام يجعله بطيئاً. المشكلة تبدأ من لحظة تحميل التطبيق: عندما يرسل السيرفر حزمة JavaScript كبيرة، يبدأ المتصفح في تحليلها وتنفيذها. هنا يظهر أول عنق زجاجة: إذا كان حجم الحزمة أكبر من ٥٠٠ كيلوبايت، سيستغرق المتصفح وقتاً طويلاً في تنفيذه، خاصة على الأجهزة الضعيفة. لكن الحجم ليس المشكلة الوحيدة — حتى التطبيقات الصغيرة يمكن أن تكون بطيئة إذا لم تُحسن إدارة الذاكرة أو الـ Event Loop.
لنأخذ مثالاً واقعياً: تطبيق e-commerce مبني بـ Angular يحتوي على ٢٠٠ مكون. عند تحميل الصفحة الرئيسية، يقوم Angular بإنشاء جميع المكونات دفعة واحدة، حتى تلك التي لا تظهر للمستخدم. هذا يعني أن المتصفح ينفذ كوداً لا يحتاجه المستخدم في تلك اللحظة، مما يستهلك ذاكرة ومعالج بلا داعٍ. المشكلة تتفاقم عندما تستخدم مكتبات خارجية مثل RxJS بشكل غير صحيح، مما يؤدي إلى إنشاء اشتراكات غير ضرورية تبقى في الذاكرة حتى بعد مغادرة المستخدم للصفحة. هذه الاشتراآت تسبب ما يسمى بـ Memory Leaks، وهي أحد أكبر أسباب بطء التطبيقات على المدى الطويل.
الـ Event Loop هو قلب JavaScript، وهو المسؤول عن تنفيذ الكود بشكل غير متزامن. لكن عندما تكتب كوداً متزامناً بشكل سيئ في Angular، فإنك تمنع الـ Event Loop من العمل بكفاءة. مثلاً، إذا استخدمت `setTimeout` مع تأخير صفر داخل حلقة تكرارية كبيرة، فأنت في الواقع تمنع المتصفح من تحديث واجهة المستخدم حتى تنتهي الحلقة. هذا ما يسمى بـ Blocking Call، وهو يسبب تجميد واجهة المستخدم تماماً. المشكلة الأكبر هي أن معظم المطورين لا يدركون أن دوال مثل `JSON.parse` أو `Array.sort` يمكن أن تكون Blocking إذا تعاملت مع بيانات كبيرة.
// مثال على Blocking Call في Angular
// هذا الكود سيجمد واجهة المستخدم لمدة ثوانٍ إذا كان data كبيراً
heavyComputation(data: any[]) {
// هذا السطر سيحجز الـ Event Loop حتى ينتهي
const sorted = data.sort((a, b) => a.value - b.value);
// هذا السطر أيضاً Blocking إذا كان الـ JSON كبيراً
const parsed = JSON.parse(JSON.stringify(sorted));
return parsed;
}
// الحل: استخدم Web Workers أو قم بتقطيع العمل
async heavyComputationOptimized(data: any[]) {
// استخدم setTimeout لتفكيك العمل على الـ Event Loop
const chunkSize = 1000;
let result = [];
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
// استخدم Promise لإعطاء الفرصة للـ Event Loop
await new Promise(resolve => setTimeout(() => {
result = result.concat(chunk.sort((a, b) => a.value - b.value));
resolve(null);
}, 0));
}
return result;
}التحميل الكسول هو أول خطوة يجب أن تتخذها لتحسين أداء Angular، لكنه ليس مجرد تقسيم التطبيق إلى وحدات. السر يكمن في كيفية تقسيم الوحدات وكيفية تحميلها. مثلاً، إذا كان لديك تطبيق يحتوي على لوحة تحكم وإعدادات وصفحة ملف شخصي، فلا تقم بتحميل جميع الوحدات عند دخول المستخدم إلى الصفحة الرئيسية. بدلاً من ذلك، قم بتحميل وحدة لوحة التحكم فقط عند الحاجة، واستخدم `preloadStrategy` لتحميل الوحدات الأخرى في الخلفية بعد تحميل الصفحة الرئيسية.
في مشروع سابق عملت عليه لشركة SaaS، قمنا بتقسيم التطبيق إلى ١٥ وحدة مختلفة، واستخدمنا استراتيجية `PreloadAllModules` مع تأخير تحميل الوحدات غير الضرورية لمدة ٥ ثوانٍ بعد تحميل الصفحة الرئيسية. النتيجة؟ انخفض وقت التحميل الأولي من ٤.٢ ثانية إلى ١.٨ ثانية، وانخفض استهلاك الذاكرة بنسبة ٤٠٪. لكن التحميل الكسول وحده ليس كافياً — يجب أيضاً تحسين كيفية تحميل الوحدات نفسها. مثلاً، استخدم `webpackChunkName` لتسمية الحزم بشكل واضح، وقم بتحليل حجم الحزم باستخدام `source-map-explorer` للتأكد من عدم وجود مكتبات مكررة.
// مثال على التحميل الكسول مع PreloadStrategy في Angular
// app-routing.module.ts
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
data: { preload: true } // استخدم هذا مع PreloadStrategy المخصصة
},
{
path: 'settings',
loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule)
}
];
// استخدم PreloadStrategy مخصصة لتحميل الوحدات في الخلفية
@Injectable({
providedIn: 'root'
})
export class CustomPreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data?.['preload']) {
// تحميل الوحدة بعد 5 ثوانٍ من تحميل الصفحة
return timer(5000).pipe(
mergeMap(() => load())
);
}
return of(null);
}
}الـ Change Detection في Angular هو سلاح ذو حدين. من ناحية، يضمن أن واجهة المستخدم دائماً محدثة، لكن من ناحية أخرى، يمكن أن يسبب إعادة رسم غير ضرورية للمكونات. الوضع الافتراضي لـ Change Detection هو `Default`، مما يعني أن Angular يفحص جميع المكونات في الشجرة عند أي حدث، سواء كان ذلك حدثاً من المستخدم أو من الـ Observable. هذا يسبب مشكلة كبيرة في التطبيقات الكبيرة، حيث يمكن أن يكون هناك مئات المكونات التي تُفحص بلا داعٍ.
الحل هو استخدام استراتيجية `OnPush`. عندما تستخدم `OnPush`، فإن Angular لن يفحص المكون إلا في حالتين: عندما يتغير المدخل (`@Input`) أو عندما يحدث حدث داخل المكون نفسه. هذا يقلل بشكل كبير من عدد المرات التي يُعاد فيها رسم المكونات. لكن هناك فخ هنا: إذا استخدمت `OnPush` مع كائنات أو مصفوفات قابلة للتغيير (`mutable`)، فلن يعمل التغيير كما تتوقع. مثلاً، إذا كان لديك مصفوفة `@Input` وقمت بتعديلها مباشرة باستخدام `push`، فلن يلاحظ Angular التغيير. الحل هو استخدام كائنات غير قابلة للتغيير (`immutable`) أو استخدام `ChangeDetectorRef` لإخبار Angular يدوياً بأن المكون يحتاج إلى إعادة رسم.
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush // استخدم OnPush
})
export class ProductListComponent {
@Input() products: Product[];
// هذا لن يعمل مع OnPush لأن المصفوفة قابلة للتغيير
addProductBad(product: Product) {
this.products.push(product); // Angular لن يلاحظ التغيير
}
// الحل: استخدم كائن غير قابل للتغيير
addProductGood(product: Product) {
this.products = [...this.products, product]; // Angular سيلاحظ التغيير
}
// أو استخدم ChangeDetectorRef
constructor(private cdr: ChangeDetectorRef) {}
addProductManual(product: Product) {
this.products.push(product);
this.cdr.detectChanges(); // أخبر Angular بإعادة رسم المكون
}
}إذا كان تطبيقك يقوم بمعالجة بيانات ثقيلة مثل تحليل الصور أو فرز جداول كبيرة، فإن استخدام الـ Main Thread لمعالجة هذه البيانات هو خطأ فادح. الـ Main Thread هو المسؤول عن تحديث واجهة المستخدم، وإذا حجزته بمهمة ثقيلة، سيجمد التطبيق تماماً. الحل هو استخدام Web Workers، وهي تسمح لك بتشغيل كود JavaScript في خلفية منفصلة عن الـ Main Thread. هذا يعني أن واجهة المستخدم ستبقى مستجيبة حتى أثناء معالجة البيانات الثقيلة.
في مشروع لشركة أدوية، كان لدينا تطبيق Angular يقوم بتحليل بيانات مرضى تحتوي على ملايين السجلات. عند محاولة فرز هذه البيانات في الـ Main Thread، تجمد التطبيق لمدة ١٠ ثوانٍ. بعد نقل المعالجة إلى Web Worker، انخفض وقت الاستجابة إلى أقل من ثانية، وأصبح التطبيق مستجيباً طوال الوقت. لكن استخدام Web Workers ليس سهلاً كما يبدو — هناك تحديات مثل نقل البيانات بين الـ Main Thread والworker، والتعامل مع الأخطاء، وضمان عدم تسرب الذاكرة.
// worker.ts
// هذا الكود يعمل داخل Web Worker
self. (e) => {
const data = e.data;
// قم بمعالجة البيانات الثقيلة هنا
const result = heavyDataProcessing(data);
// أرسل النتيجة مرة أخرى للـ Main Thread
self.postMessage(result);
};
function heavyDataProcessing(data: any[]) {
// مثال: فرز البيانات الثقيلة
return data.sort((a, b) => a.value - b.value);
}
// main.component.ts
// استخدام Web Worker في Angular
@Component({
selector: 'app-data-processor',
template: `<button (click)="processData()">معالجة البيانات</button>`
})
export class DataProcessorComponent {
worker: Worker;
constructor() {
// تحقق من دعم المتصفح لـ Web Workers
if (typeof Worker !== 'undefined') {
this.worker = new Worker('./data.worker', { type: 'module' });
this.worker.onmessage = ({ data }) => {
console.log('النتيجة من Worker:', data);
};
}
}
processData() {
const data = [...]; // البيانات الكبيرة
this.worker.postMessage(data);
}
ngOnDestroy() {
// لا تنسَ إنهاء Worker عند تدمير المكون
this.worker?.terminate();
}
}حجم حزمة JavaScript هو أحد أكبر العوامل التي تؤثر على أداء Angular. كلما كانت الحزمة أكبر، كلما استغرق المتصفح وقتاً أطول لتحليلها وتنفيذها. المشكلة هي أن معظم المطورين لا يعرفون كيف يقللون حجم الحزمة بشكل فعال. مثلاً، استخدام مكتبة مثل `lodash` كاملة بدلاً من استيراد الدوال التي تحتاجها فقط هو خطأ شائع. أيضاً، عدم استخدام Tree Shaking بشكل صحيح يمكن أن يؤدي إلى تضمين كود غير مستخدم في الحزمة النهائية.
في أحد المشاريع، وجدنا أن حزمة التطبيق كانت تحتوي على ٣ نسخ مختلفة من مكتبة `moment.js`، بالإضافة إلى كود غير مستخدم من مكتبات أخرى. بعد تحليل الحزمة باستخدام `webpack-bundle-analyzer`، اكتشفنا أن حجم الحزمة كان ٢.٤ ميجابايت، منها ١.١ ميجابايت كود غير مستخدم. بعد التنظيف، انخفض حجم الحزمة إلى ٨٠٠ كيلوبايت، وانخفض وقت التحميل الأولي من ٣.٧ ثانية إلى ١.٢ ثانية. إليك بعض النصائح العملية لتقليل حجم الحزمة:
// مثال على استيراد دقيق لتقليل حجم الحزمة
// بدلاً من هذا:
import * as _ from 'lodash';
// افعل هذا:
import { debounce } from 'lodash-es';
// بدلاً من هذا:
import moment from 'moment';
// افعل هذا:
import { format } from 'date-fns';
// في angular.json، تأكد من تفعيل التحسينات
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}واحدة من أكبر الأخطاء التي يقع فيها المطورون هي رسم مكونات لا يراها المستخدم. مثلاً، إذا كان لديك قائمة تحتوي على ١٠٠٠ عنصر، ورسم جميع العناصر دفعة واحدة، فسيستغرق المتصفح وقتاً طويلاً في تحديث DOM. الحل هو استخدام تقنيات مثل Virtual Scrolling، التي ترسم فقط العناصر التي تظهر في نافذة العرض. مكتبات مثل `cdk/scrolling` في Angular توفر هذه الميزة بشكل جاهز، لكنها ليست كافية وحدها — يجب أيضاً تحسين كيفية تحديث البيانات في القائمة.
في تطبيق تواصل اجتماعي عملنا عليه، كان لدينا قائمة تحتوي على آلاف المنشورات. عند محاولة عرض جميع المنشورات دفعة واحدة، تجمد التطبيق لمدة ٥ ثوانٍ. بعد استخدام Virtual Scrolling، انخفض وقت الاستجابة إلى أقل من ٢٠٠ مللي ثانية. لكن هناك فخ هنا: إذا كانت البيانات التي تعرضها تتغير بشكل متكرر، فإن Virtual Scrolling يمكن أن يسبب وميضاً في واجهة المستخدم. الحل هو استخدام `trackBy` مع `*ngFor` لتقليل عدد المرات التي يُعاد فيها رسم العناصر. أيضاً، تجنب استخدام دوال معقدة داخل `*ngIf` أو `*ngFor`، لأنها تُنفذ في كل دورة من Change Detection.
@Component({
selector: 'app-post-list',
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let post of posts; let i = index; trackBy: trackById"
class="post">
{{ post.title }}
</div>
</cdk-virtual-scroll-viewport>
`
})
export class PostListComponent {
posts: Post[] = [];
// استخدم trackBy لتقليل إعادة الرسم
trackById(index: number, post: Post): number {
return post.id;
}
// تجنب استخدام دوال معقدة داخل القالب
// ❌ سيء: <div *ngIf="isPostVisible(post)">
// ✅ جيد: قم بحساب isVisible مسبقاً
}تحسين أداء Angular ليس مجرد تطبيق نصائح عشوائية — إنه فهم عميق لكيفية عمل الإطار تحت الغطاء، وكيفية تفاعله مع المتصفح. إليك أهم النصائح التي يجب أن تأخذها معك:
في النهاية، تحسين أداء Angular هو عملية مستمرة. لا تنتظر حتى يصبح التطبيق بطيئاً — ابدأ بتطبيق هذه التقنيات منذ اليوم الأول. تذكر أن كل مللي ثانية مهمة، وكل كيلوبايت في الحزمة يؤثر على تجربة المستخدم. إذا طبقت هذه التقنيات بشكل صحيح، ستحول تطبيقك من بطيء إلى سريع، ومن مزعج إلى ممتع للمستخدمين.