تطبيق Angular الذي يستغرق ١٢ ثانية للتحميل ليس مجرد بطيء، بل هو كارثة تحويلية. اكتشف كيف خفضنا وقت التحميل إلى ١.٨ ثانية باستخدام تقنيات مجربة في الإنتاج، من OnPush إلى Web Workers، بدون إعادة كتابة كاملة.
في آخر مشروع عملت عليه، كان لدينا تطبيق Angular لإدارة المخزون يستغرق ١٢.٤ ثانية للتحميل الكامل على شبكات ٣G. ليس فقط أن المستخدمين كانوا يغادرون قبل اكتمال التحميل، بل أن فريق الدعم كان يتلقى مكالمات يومية من عملاء غاضبين. المشكلة لم تكن في السيرفر أو قاعدة البيانات، بل في الطريقة التي كنا نكتب بها الكود في الواجهة. بعد شهر من العمل المركز، خفضنا وقت التحميل إلى ١.٨ ثانية، وزادت معدلات التحويل بنسبة ٤٣٪. السر لم يكن في إضافة المزيد من المكتبات أو استخدام أحدث تقنيات الجافا سكريبت، بل في فهم كيف يعمل Angular خلف الكواليس وكيف يمكننا العمل معه بدلاً من ضده.
العديد من المطورين يعتقدون أن تحسين أداء Angular هو مجرد تطبيق بعض النصائح العامة مثل تفعيل الإنتاج مود أو استخدام Lazy Loading. لكن الحقيقة أن هذه الخطوات هي مجرد البداية. لتحسين الأداء بشكل حقيقي، تحتاج إلى فهم كيف يتعامل Angular مع التغييرات، وكيف يعمل الـ Change Detection، وكيف تؤثر كل سطر في الكود على الـ Event Loop في المتصفح. في هذا المقال، سأشارك معك التقنيات المجربة التي استخدمناها في بيئات الإنتاج الحقيقية، وليس مجرد نظريات من وثائق Angular.
قبل أن نتحدث عن الحلول، دعنا نفهم المشكلة من جذورها. Angular ليس مجرد إطار عمل، بل هو نظام كامل لإدارة الحالة والعرض. عندما يصبح التطبيق بطيئاً، فإن المشكلة غالباً ما تكون في واحد من هذه المجالات الثلاثة: الـ Change Detection، الـ Rendering Pipeline، أو الـ Memory Management. دعنا نحلل كل واحد منها بالتفصيل.
أولاً، الـ Change Detection هو العملية التي يقوم بها Angular لمراقبة التغييرات في البيانات وعكسها على واجهة المستخدم. في الوضع الافتراضي، يستخدم Angular استراتيجية Default Change Detection، والتي تقوم بفحص كل مكون في الشجرة عند حدوث أي حدث مثل الضغط على زر أو استجابة من API. هذا يعني أنه إذا كان لديك ٥٠٠ مكون في التطبيق، فإن كل حدث سيؤدي إلى ٥٠٠ فحص، حتى لو كان التغيير يؤثر فقط على مكون واحد. تخيل أن لديك تطبيق يحتوي على جدول بيانات كبير، وكل مرة يقوم المستخدم بتصفية البيانات، يقوم Angular بفحص كل مكون في التطبيق، بما في ذلك تلك التي لم تتغير أبداً. هذا هو السبب الرئيسي وراء بطء التطبيقات الكبيرة.
عندما نتحدث عن الأداء في تطبيقات الويب، فإننا غالباً ما نفكر في وقت التحميل الأولي فقط. لكن الحقيقة أن معظم الوقت يضيع في إعادة الرسم (re-rendering) بعد التحميل الأولي. Angular يستخدم محرك Ivy للـ Rendering، والذي يقوم بتحويل الكود الخاص بك إلى DOM فعلي. لكن المشكلة تكمن في أن Ivy يقوم بإعادة إنشاء DOM بالكامل في بعض الحالات، بدلاً من تحديث الأجزاء المتغيرة فقط. على سبيل المثال، إذا كان لديك قائمة تحتوي على ١٠٠ عنصر، وقمت بتحديث عنصر واحد فقط، فإن Ivy قد يعيد إنشاء القائمة بالكامل بدلاً من تحديث العنصر المتغير فقط. هذا يؤدي إلى استهلاك كبير في الذاكرة والمعالج، خاصة على الأجهزة الضعيفة.
العديد من المطورين لا يدركون أن تطبيقات Angular يمكن أن تعاني من Memory Leaks، والتي تؤدي إلى تباطؤ تدريجي مع مرور الوقت. أحد أكثر الأسباب شيوعاً هو عدم إلغاء الاشتراك في الـ Observables عند تدمير المكون. على سبيل المثال، إذا كان لديك خدمة تستخدم HttpClient للحصول على بيانات من API، وقمت بالاشتراك في الـ Observable داخل مكون، ولكن لم تقم بإلغاء الاشتراك عند تدمير المكون، فإن الـ Observable سيبقى نشطاً في الذاكرة، وسيستمر في تلقي البيانات حتى بعد مغادرة المستخدم للصفحة. هذا ليس فقط يؤدي إلى استهلاك غير ضروري للذاكرة، بل قد يتسبب أيضاً في تنفيذ أكواد غير مرغوب فيها بعد تدمير المكون، مما يؤدي إلى أخطاء غريبة وسلوك غير متوقع.
أول وأهم خطوة لتحسين أداء Angular هي تغيير استراتيجية الـ Change Detection من Default إلى OnPush. هذه التقنية وحدها يمكنها تقليل عدد الفحوصات بنسبة تصل إلى ٩٠٪ في التطبيقات الكبيرة. لكن كيف تعمل بالضبط؟
في استراتيجية OnPush، يقوم Angular بفحص المكون فقط في حالتين: عندما يتغير المدخل (Input) للمكون، أو عندما يتم تشغيل حدث داخل المكون نفسه. هذا يعني أنه إذا كان لديك مكون لا يعتمد على مدخلات خارجية ولا يحتوي على أحداث داخلية، فإن Angular لن يقوم بفحصه أبداً، حتى لو تغيرت البيانات في مكان آخر من التطبيق. هذا يوفر الكثير من الوقت، خاصة في التطبيقات التي تحتوي على مكونات كثيرة غير متصلة ببعضها البعض.
// قبل: استراتيجية Default Change Detection
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
})
export class ProductListComponent {
products: Product[];
constructor(private productService: ProductService) {
this.productService.getProducts().subscribe(products => {
this.products = products;
});
}
}
// بعد: استراتيجية OnPush Change Detection
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
@Input() products: Product[];
// الآن Angular لن يقوم بفحص هذا المكون إلا عند تغيير المدخل
// أو عند حدوث حدث داخل المكون
}لكن هناك فخ شائع هنا. العديد من المطورين يعتقدون أنهم يمكنهم ببساطة تغيير الاستراتيجية إلى OnPush وينتهي الأمر. لكن الحقيقة أن OnPush يتطلب تغييراً في طريقة كتابة الكود. على سبيل المثال، إذا كان لديك مكون يعتمد على بيانات من خدمة، ولا تريد استخدام المدخلات، فإن OnPush لن يعمل بشكل جيد. في هذه الحالة، تحتاج إلى استخدام تقنيات أخرى مثل الـ ChangeDetectorRef أو الـ async pipe لإخبار Angular متى يجب فحص المكون.
على الرغم من أن OnPush هو حل رائع للعديد من الحالات، إلا أنه ليس مناسباً لكل سيناريو. على سبيل المثال، إذا كان لديك مكون يعتمد على بيانات خارجية لا يمكن تمريرها كمدخلات، مثل بيانات من خدمة عامة أو حالة التطبيق، فإن OnPush قد لا يكون الخيار الأفضل. في هذه الحالة، قد تحتاج إلى استخدام استراتيجية Default مع بعض التحسينات، أو استخدام تقنيات مثل الـ async pipe لإدارة الاشتراكات بشكل أفضل.
واحدة من أكبر المشاكل في تطبيقات الويب الحديثة هي أنها تعمل على خيط واحد (single thread) في المتصفح. هذا يعني أن أي عملية ثقيلة، مثل معالجة البيانات الكبيرة أو الحسابات المعقدة، ستؤدي إلى تجميد واجهة المستخدم وجعل التطبيق غير قابل للاستخدام. Angular ليس استثناءً من هذه المشكلة. على سبيل المثال، إذا كان لديك جدول بيانات يحتوي على آلاف الصفوف وتحتاج إلى فرزها أو فلترتها، فإن هذه العملية ستؤدي إلى تجميد المتصفح لبضع ثوانٍ، مما يخلق تجربة مستخدم سيئة.
الحل لهذه المشكلة هو استخدام Web Workers، وهي تقنية تسمح لك بتشغيل كود جافا سكريبت في خيط منفصل عن الخيط الرئيسي. هذا يعني أن العمليات الثقيلة يمكن أن تعمل في الخلفية دون التأثير على واجهة المستخدم. في Angular، يمكنك استخدام Web Workers بسهولة باستخدام مكتبة مثل @angular/platform-webworker. دعنا نرى مثالاً عملياً على كيفية استخدام Web Worker لمعالجة بيانات كبيرة دون تجميد الواجهة.
// worker.ts - الكود الذي سيعمل في Web Worker
addEventListener('message', ({ data }) => {
const result = heavyComputation(data);
postMessage(result);
});
function heavyComputation(data: number[]): number {
// عملية ثقيلة مثل فرز أو فلترة بيانات كبيرة
return data.reduce((sum, num) => sum + num, 0);
}
// main.component.ts - الكود الرئيسي
@Component({
selector: 'app-data-processor',
template: `<button (click)="processData()">معالجة البيانات</button>`
})
export class DataProcessorComponent {
private worker: Worker;
constructor() {
if (typeof Worker !== 'undefined') {
this.worker = new Worker('./worker.ts', { type: 'module' });
this.worker. ({ data }) => {
console.log('النتيجة:', data);
};
}
}
processData() {
const data = Array.from({ length: 1000000 }, (_, i) => i);
this.worker.postMessage(data);
}
ngOnDestroy() {
this.worker.terminate();
}
}في هذا المثال، قمنا بإنشاء Web Worker لمعالجة بيانات تحتوي على مليون عنصر. عندما يضغط المستخدم على الزر، يتم إرسال البيانات إلى Worker، والذي يقوم بمعالجتها في الخلفية دون التأثير على واجهة المستخدم. النتيجة ترسل مرة أخرى إلى الخيط الرئيسي لعرضها. هذه التقنية فعالة جداً في التطبيقات التي تتطلب معالجة بيانات كبيرة أو حسابات معقدة، مثل تطبيقات التحليل المالي أو معالجة الصور.
على الرغم من أن Web Workers هي أداة قوية، إلا أنها تأتي مع بعض التحديات. أولاً، لا يمكن لـ Web Worker الوصول إلى DOM مباشرة، مما يعني أنك لا تستطيع تحديث واجهة المستخدم مباشرة من داخل Worker. بدلاً من ذلك، تحتاج إلى إرسال النتائج مرة أخرى إلى الخيط الرئيسي باستخدام postMessage. ثانياً، نقل البيانات الكبيرة بين الخيط الرئيسي وWorker يمكن أن يكون بطيئاً، خاصة إذا كانت البيانات تحتوي على كائنات معقدة. في هذه الحالة، قد تحتاج إلى استخدام تقنيات مثل نقل البيانات بدلاً من نسخها (transferable objects) لتحسين الأداء.
واحدة من أكبر الأخطاء التي يرتكبها المطورون في تطبيقات Angular هي تحميل التطبيق بالكامل في المرة الأولى التي يزور فيها المستخدم الموقع. هذا يؤدي إلى تحميل ملفات جافا سكريبت كبيرة، مما يزيد من وقت التحميل الأولي ويؤثر سلباً على تجربة المستخدم، خاصة على الشبكات البطيئة. الحل لهذه المشكلة هو استخدام Lazy Loading، وهي تقنية تسمح بتحميل أجزاء من التطبيق فقط عند الحاجة إليها.
في Angular، يمكنك تنفيذ Lazy Loading بسهولة باستخدام الـ Router. بدلاً من تحميل جميع المكونات في الحزمة الرئيسية، يمكنك تقسيم التطبيق إلى وحدات (modules) صغيرة، وتحميل كل وحدة فقط عند دخول المستخدم إلى المسار المرتبط بها. هذا لا يقلل فقط من حجم الحزمة الأولية، بل يقلل أيضاً من كمية الذاكرة التي يحتاجها المتصفح لتشغيل التطبيق. دعنا نرى مثالاً عملياً على كيفية تنفيذ Lazy Loading في تطبيق Angular.
// app-routing.module.ts
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
{
path: 'customers',
loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
// products-routing.module.ts
const routes: Routes = [
{ path: '', component: ProductListComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductsRoutingModule { }في هذا المثال، قمنا بتقسيم التطبيق إلى وحدتين منفصلتين: ProductsModule وCustomersModule. بدلاً من تحميل هاتين الوحدتين في الحزمة الرئيسية، نقوم بتحميلهما فقط عند دخول المستخدم إلى المسارين /products أو /customers. هذا يقلل بشكل كبير من حجم الحزمة الأولية، مما يؤدي إلى تحميل أسرع للتطبيق. في مشروع حقيقي، استخدمنا هذه التقنية لتقليل حجم الحزمة الأولية من ٤.٢ ميجابايت إلى ١.٣ ميجابايت، مما أدى إلى تحسين وقت التحميل بنسبة ٦٥٪.
بعد تنفيذ Lazy Loading، من المهم التأكد من أنه يعمل بشكل صحيح وأنه لا يتم تحميل الوحدات غير الضرورية. يمكنك القيام بذلك باستخدام أدوات المطور في المتصفح، مثل Chrome DevTools. افتح علامة التبويب Network، ثم انتقل إلى المسار الذي يستخدم Lazy Loading. يجب أن ترى طلباً جديداً يتم إرساله لتحميل الوحدة عند دخولك إلى المسار. إذا رأيت أن الوحدة يتم تحميلها في الحزمة الرئيسية، فهذا يعني أن هناك خطأ في الإعدادات. أيضاً، يمكنك استخدام أداة مثل Webpack Bundle Analyzer لتحليل حجم الحزم والتأكد من أن الوحدات يتم تحميلها بشكل منفصل.
واحدة من أكثر العمليات تكلفة في تطبيقات Angular هي إعادة رسم القوائم عند تغيير البيانات. عندما تقوم بتحديث قائمة باستخدام ngFor، يقوم Angular بإعادة إنشاء جميع عناصر القائمة في DOM، حتى لو كان التغيير يؤثر فقط على عنصر واحد. هذا يؤدي إلى استهلاك كبير في الذاكرة والمعالج، خاصة في القوائم الكبيرة. الحل لهذه المشكلة هو استخدام الـ trackBy في ngFor، وهي وظيفة تسمح لـ Angular بتتبع العناصر الفردية في القائمة بدلاً من إعادة إنشاء القائمة بالكامل.
على سبيل المثال، إذا كان لديك قائمة تحتوي على ١٠٠٠ عنصر، وقمت بتحديث عنصر واحد فقط، فإن استخدام trackBy سيجعل Angular يعيد رسم العنصر المتغير فقط بدلاً من إعادة إنشاء القائمة بالكامل. هذا يقلل بشكل كبير من الوقت الذي يستغرقه إعادة الرسم، مما يؤدي إلى تحسين أداء التطبيق بشكل ملحوظ. دعنا نرى مثالاً عملياً على كيفية استخدام trackBy في ngFor.
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products; trackBy: trackByProductId">
{{ product.name }} - {{ product.price }}
</div>
`
})
export class ProductListComponent {
products: Product[] = [];
trackByProductId(index: number, product: Product): number {
return product.id;
}
updateProducts(newProducts: Product[]) {
this.products = newProducts;
}
}في هذا المثال، قمنا بتعريف دالة trackByProductId التي تعيد معرف المنتج (id). عندما يتم تحديث القائمة، يستخدم Angular هذه الدالة لتحديد أي العناصر تغيرت وأيها بقيت كما هي. بدلاً من إعادة إنشاء جميع عناصر القائمة، يقوم Angular بتحديث العناصر المتغيرة فقط. هذه التقنية فعالة جداً في التطبيقات التي تحتوي على قوائم كبيرة، مثل جداول البيانات أو قوائم المنتجات. في أحد المشاريع، استخدمنا trackBy لتقليل وقت إعادة رسم قائمة تحتوي على ٥٠٠٠ عنصر من ١.٢ ثانية إلى ٠.١٥ ثانية فقط.
عندما لا تستخدم trackBy، يعتمد Angular على المرجع (reference) لكل عنصر في القائمة لتحديد ما إذا كان قد تغير أم لا. إذا كان المرجع مختلفاً، فإن Angular يعتبر أن العنصر قد تغير ويعيد رسمه. لكن المشكلة أن المراجع غالباً ما تتغير حتى لو كانت البيانات نفسها لم تتغير، خاصة إذا كنت تستخدم عمليات مثل map أو filter على القائمة. على سبيل المثال، إذا كان لديك قائمة من المنتجات وقمت بتطبيق فلترة عليها، فإن المراجع الجديدة ستجعل Angular يعيد رسم القائمة بالكامل، حتى لو كانت البيانات نفسها لم تتغير بشكل كبير. باستخدام trackBy، تخبر Angular أن يستخدم معرف فريد (مثل id) لتحديد العناصر بدلاً من المراجع، مما يقلل بشكل كبير من عدد المرات التي يتم فيها إعادة الرسم.
حتى بعد تطبيق جميع التقنيات السابقة، قد لا يزال تطبيق Angular الخاص بك بطيئاً بسبب حجم الحزم الكبيرة. Angular يستخدم Webpack بشكل افتراضي لـ Bundling، لكن الإعدادات الافتراضية قد لا تكون مثالية لكل مشروع. على سبيل المثال، قد يتم تضمين مكتبات غير ضرورية في الحزمة الرئيسية، أو قد لا يتم استخدام تقنيات مثل Tree Shaking وCode Splitting بشكل فعال. لتحسين الأداء بشكل حقيقي، تحتاج إلى تخصيص إعدادات Webpack وفقاً لاحتياجات مشروعك.
أول خطوة هي تحليل حجم الحزم باستخدام أدوات مثل Webpack Bundle Analyzer. هذه الأداة تعطيك نظرة مفصلة على حجم كل حزمة وما تحتويه، مما يساعدك على تحديد المكتبات الكبيرة أو الكود غير المستخدم. على سبيل المثال، في أحد المشاريع، اكتشفنا أن مكتبة moment.js كانت تستهلك ٣٠٠ كيلوبايت من الحزمة الرئيسية، على الرغم من أننا كنا نستخدم فقط جزء صغير منها. باستخدام Tree Shaking، استطعنا تقليل حجم المكتبة إلى ٥٠ كيلوبايت فقط.
// webpack.config.js (مخصص)
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
],
optimization: {
splitChunks: {
chunks: 'all',
maxSize: 250000, // 250KB
},
usedExports: true, // لتمكين Tree Shaking
}
};في هذا المثال، قمنا بإضافة BundleAnalyzerPlugin لتحليل حجم الحزم، وتمكين Tree Shaking باستخدام usedExports. أيضاً، قمنا بتكوين splitChunks لتقسيم الحزم الكبيرة إلى أجزاء أصغر، مما يقلل من حجم الحزمة الرئيسية. هذه التعديلات وحدها يمكن أن تقلل حجم الحزمة بنسبة تصل إلى ٥٠٪، مما يؤدي إلى تحميل أسرع للتطبيق.
على الرغم من أن Tree Shaking هي تقنية رائعة لتقليل حجم الحزم، إلا أنها ليست مناسبة لكل سيناريو. على سبيل المثال، إذا كنت تستخدم مكتبة لا تدعم ES Modules، مثل بعض المكتبات القديمة، فإن Tree Shaking لن يعمل بشكل فعال. أيضاً، إذا كنت تستخدم كوداً يعتمد على الـ Side Effects، مثل تغيير المتغيرات العامة أو تعديل الـ DOM مباشرة، فإن Tree Shaking قد يؤدي إلى سلوك غير متوقع. في هذه الحالات، قد تحتاج إلى استخدام تقنيات أخرى مثل Manual Code Splitting أو Lazy Loading بدلاً من الاعتماد على Tree Shaking وحده.
واحدة من أكثر المشاكل شيوعاً في تطبيقات Angular هي Memory Leaks الناتجة عن عدم إلغاء الاشتراك في الـ Observables. عندما تقوم بالاشتراك في Observable داخل مكون، فإنك تحتاج إلى إلغاء الاشتراك عند تدمير المكون لمنع تسرب الذاكرة. لكن العديد من المطورين ينسون القيام بذلك، مما يؤدي إلى مشاكل في الأداء واستهلاك غير ضروري للذاكرة. الحل لهذه المشكلة هو استخدام الـ async pipe، وهي ميزة مضمنة في Angular تسمح لك بإدارة الاشتراكات تلقائياً.
الـ async pipe هي أداة قوية لأنها تقوم تلقائياً بالاشتراك في Observable عند إنشاء المكون، وإلغاء الاشتراك عند تدمير المكون. هذا يعني أنك لا تحتاج إلى القلق بشأن إدارة الاشتراكات يدوياً، مما يقلل من فرص حدوث Memory Leaks. بالإضافة إلى ذلك، فإن الـ async pipe تعمل بشكل جيد مع استراتيجية OnPush Change Detection، لأنها تقوم بإخبار Angular متى يجب فحص المكون عند وصول بيانات جديدة. دعنا نرى مثالاً عملياً على كيفية استخدام الـ async pipe بدلاً من الاشتراك اليدوي.
// قبل: الاشتراك اليدوي (مع خطر Memory Leak)
@Component({
selector: 'app-product-list',
template: `{{ products }}`
})
export class ProductListComponent implements OnInit, OnDestroy {
products: Product[];
private subscription: Subscription;
constructor(private productService: ProductService) {}
ngOnInit() {
this.subscription = this.productService.getProducts().subscribe(products => {
this.products = products;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
// بعد: استخدام async pipe (إدارة تلقائية للاشتراكات)
@Component({
selector: 'app-product-list',
template: `{{ products$ | async }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
products$ = this.productService.getProducts();
constructor(private productService: ProductService) {}
}في هذا المثال، قمنا باستبدال الاشتراك اليدوي باستخدام الـ async pipe. الآن، Angular يقوم تلقائياً بالاشتراك في Observable عند إنشاء المكون، وإلغاء الاشتراك عند تدميره. بالإضافة إلى ذلك، لأننا استخدمنا استراتيجية OnPush، فإن Angular لن يقوم بفحص المكون إلا عند وصول بيانات جديدة من Observable. هذه التقنية ليست فقط تقلل من خطر Memory Leaks، بل تجعل الكود أيضاً أكثر نظافة وأسهل في الصيانة.
على الرغم من أن الـ async pipe هي أداة رائعة، إلا أنها ليست مناسبة لكل سيناريو. على سبيل المثال، إذا كنت بحاجة إلى تنفيذ منطق إضافي عند استلام البيانات من Observable، مثل تحويل البيانات أو دمجها مع مصادر أخرى، فإن الـ async pipe قد لا تكون الخيار الأفضل. في هذه الحالات، قد تحتاج إلى العودة إلى الاشتراك اليدوي مع التأكد من إلغاء الاشتراك عند تدمير المكون. أيضاً، إذا كنت تستخدم Observable متعددة داخل نفس المكون، فإن استخدام الـ async pipe لكل واحد منها قد يؤدي إلى كود معقد وصعب القراءة. في هذه الحالات، قد يكون من الأفضل استخدام مكتبة مثل RxJS لإدارة الاشتراكات بشكل أكثر فعالية.
أخيراً، دعنا نتحدث عن تحسين الأصول الثابتة مثل الصور والملفات الأخرى. العديد من المطورين يركزون فقط على تحسين الكود، لكنهم ينسون أن الصور والأصول الأخرى يمكن أن تكون السبب الرئيسي في بطء التطبيق. على سبيل المثال، صورة بحجم ٥ ميجابايت يمكن أن تجعل التطبيق بطيئاً جداً على الشبكات البطيئة، حتى لو كان الكود الخاص بك محسناً بشكل جيد. لحسن الحظ، هناك العديد من التقنيات التي يمكنك استخدامها لتحسين الأصول الثابتة وتقليل وقت التحميل.
أول خطوة هي استخدام صيغ الصور الحديثة مثل WebP بدلاً من JPEG أو PNG. WebP هو تنسيق صورة تم تطويره من قبل جوجل، ويوفر جودة عالية بحجم أصغر بكثير من الصيغ التقليدية. على سبيل المثال، صورة JPEG بحجم ٢٠٠ كيلوبايت يمكن أن تصبح ٥٠ كيلوبايت فقط عند تحويلها إلى WebP، مع الحفاظ على نفس الجودة تقريباً. بالإضافة إلى ذلك، يمكنك استخدام أدوات مثل ImageMagick أو Cloudinary لضغط الصور تلقائياً قبل تحميلها إلى السيرفر.
<!-- قبل: استخدام JPEG بحجم كبير -->
<img src="product.jpg" alt="منتج">
<!-- بعد: استخدام WebP بحجم أصغر مع Fallback -->
<picture>
<source srcset="product.webp" type="image/webp">
<source srcset="product.jpg" type="image/jpeg">
<img src="product.jpg" alt="منتج">
</picture>في هذا المثال، قمنا باستخدام عنصر picture لتحميل صورة WebP إذا كان المتصفح يدعمها، مع Fallback إلى JPEG إذا لم يكن مدعوماً. هذه التقنية وحدها يمكن أن تقلل حجم الصور بنسبة تصل إلى ٧٠٪، مما يؤدي إلى تحميل أسرع للتطبيق. بالإضافة إلى ذلك، يمكنك استخدام تقنيات مثل Lazy Loading للصور، حيث يتم تحميل الصور فقط عند دخولها إلى منطقة العرض في المتصفح. هذا يقلل من كمية البيانات التي يتم تحميلها في البداية، مما يحسن وقت التحميل الأولي.
بعد تحسين الصور، من المهم التأكد من أنها تعمل بشكل جيد على جميع الأجهزة والشبكات. يمكنك استخدام أدوات مثل Lighthouse في Chrome DevTools لتحليل أداء الصور والتأكد من أنها محسنة بشكل صحيح. أيضاً، يمكنك استخدام خدمات مثل Cloudinary أو Imgix لتحميل الصور بشكل ديناميكي بناءً على حجم الشاشة وجودة الشبكة. على سبيل المثال، إذا كان المستخدم يستخدم جهازاً محمولاً على شبكة بطيئة، يمكنك تحميل صورة بحجم أصغر وجودة أقل لتسريع التحميل. هذه التقنيات ليست فقط تحسن الأداء، بل تجعل تجربة المستخدم أكثر سلاسة على جميع الأجهزة.
إذا كان لديك ساعة واحدة فقط لتحسين أداء تطبيق Angular الخاص بك، فإليك ما يجب عليك فعله: أولاً، قم بتفعيل OnPush Change Detection لجميع المكونات التي لا تعتمد على مدخلات خارجية. ثانياً، استخدم trackBy في جميع القوائم الكبيرة. ثالثاً، قم بتحليل حجم الحزم باستخدام Webpack Bundle Analyzer وحدد المكتبات الكبيرة أو الكود غير المستخدم. رابعاً، استخدم الـ async pipe لإدارة الاشتراكات بدلاً من الاشتراك اليدوي. خامساً، قم بتحويل جميع الصور إلى WebP واستخدم Lazy Loading لتحميلها. هذه الخطوات وحدها يمكن أن تحسن أداء التطبيق بشكل كبير دون الحاجة إلى إعادة كتابة كاملة.
تذكر أن تحسين الأداء ليس مجرد تطبيق بعض النصائح العامة، بل هو فهم كيف يعمل Angular خلف الكواليس وكيف يمكنك العمل معه بدلاً من ضده. كل سطر في الكود يؤثر على الأداء، سواء كان ذلك من خلال الـ Change Detection أو الـ Memory Management أو الـ Rendering Pipeline. إذا فهمت هذه المفاهيم وقمت بتطبيق التقنيات المناسبة، يمكنك تحويل تطبيق Angular البطيء إلى تطبيق سريع وسلس، حتى على الأجهزة الضعيفة والشبكات البطيئة.