أداء الواجهة الأمامية في تطبيقات المؤسسات: ميزانيات لا بطولات
أداء الواجهة الأمامية في تطبيقات المؤسسات: لدى المستخدمين الحقيقيين، وميزانيات تُفشل ، ومسارات كسولة، وجداول طويلة، وصور، وبيانات الميدان.

نادراً ما يصبح تطبيق المؤسسة بطيئاً في إصدار واحد. إنه يبطؤ اعتماديةً بعد أخرى، وعنصرَ واجهة بعد آخر، وجدولاً «مؤقتاً» بعد آخر، ولا أحد مسؤول عن المجموع. ثم ينتظر موظف في أحد فروع البنك، على حاسوب قديم، ظهور شاشة المعاملات، فيتحوّل الإصلاح إلى مشروع.
الأداء في قاعدة كود تعيش سنوات طويلة ليس جهداً بطولياً في سباق قصير. إنه مجموعة حدود يتحقق منها البناء كل يوم، وطريقة لرؤية ما يشعر به المستخدمون الحقيقيون.
المفاهيم القليلة التي تهمّ
Core Web Vitals، مقيسةً لدى المستخدمين البطيئين أيضاً. مقاييس Core Web Vitals ثلاثة أرقام تصف إحساس المستخدم بالصفحة. (Largest Contentful Paint): سرعة ظهور المحتوى الرئيسي، وتكون جيدة في حدود 2.5 ثانية. (Interaction to Next Paint): سرعة استجابة الصفحة للنقرة، وتكون جيدة عند 200 ميلي ثانية أو أقل. (Cumulative Layout Shift): مقدار قفز عناصر الصفحة أثناء التحميل، ويكون جيداً عند 0.1 أو أقل. ويقترح web.dev قياس «المئين الخامس والسبعين من عمليات تحميل الصفحة، مع الفصل بين الأجهزة المحمولة وأجهزة سطح المكتب». والمئين الخامس والسبعون () يعني: رتّب كل عمليات التحميل من الأسرع إلى الأبطأ، وخذ العملية التي تقع عند ثلاثة أرباع الطريق. فإذا كانت جيدة، حصل ثلاثة من كل أربعة مستخدمين على صفحة جيدة. المتوسط يخفي الربع الأبطأ، أما هذا القياس فلا يخفيه.
الصورة: tt-tech.io، مقتبسة من web.dev، Web Vitals، بترخيص .
INP للشاشات المليئة بالبيانات. يقول web.dev إن INP «يقيّم الاستجابة الإجمالية للصفحة لتفاعلات المستخدم». تطبيق المكتب الخلفي (back-office) يُحمَّل مرة واحدة في الصباح، ثم يُنقر عليه طوال اليوم: تصفية، وترتيب، وفتح صف. هناك يشعر مستخدمو المؤسسات بالبطء، لذلك راقب INP أولاً في تلك الشاشات.
الميزانيات داخل البناء. ميزانية الأداء () هي «مجموعة حدود مفروضة على المقاييس التي تؤثر في أداء الموقع». وهي مدمجة في : قسم budgets في إعدادات البناء، مع maximumWarning و maximumError لكل نوع. ويعطي النوع initial افتراضياً تحذيراً عند 500 كيلوبايت وخطأً عند 1 ميغابايت. الميزانية المكتوبة في صفحة توثيق داخلية تتآكل مع الوقت. أما الميزانية داخل البناء فهي القاعدة نفسها لكل الفرق.
التحميل الكسول لكل مسار ولكل عنصر واجهة. مسار كسول واحد () لكل ميزة يُبقي كود كل فريق خارج التحميل الأول للفرق الأخرى. وداخل الشاشة، تعمل كتل @defer على «تقليص حجم الحزمة الأولية» بتأجيل الكود «غير الضروري تماماً للعرض الأولي للصفحة». ويفعل الشيء نفسه بـ lazy مع بديل مؤقت في <Suspense>.
القوائم والجداول الطويلة. يعرض <cdk-virtual-scroll-viewport> من القوائم الطويلة «بعرض العناصر التي تتسع لها الشاشة فقط». وفي قائمة معاملات من عشرة آلاف صف، اسأل أولاً: هل ينبغي أن يقسّم الخادم النتائج إلى صفحات؟ التمرير الافتراضي () يصلح العرض، لكنه لا يصلح تنزيل عشرة آلاف صف.
كلفة العرض. مع اعتماد و افتراضياً في إصدارات Angular الحديثة، اختفى جزء كبير من كلفة اكتشاف التغييرات () القديمة. ما يبقى هو القوائم: يتيح track لـ Angular تنفيذ «الحد الأدنى الضروري من عمليات الـ DOM حين تتغير البيانات»، أما التتبع بمرجع الكائن فـ«قد يؤدي إلى تحديثات عرض أبطأ بكثير».
الصور في التطبيقات الكثيرة الصور. حين تعرض الشاشة صوراً كثيرة، كصور المنتجات والوثائق الممسوحة ضوئياً والصور الرمزية، استخدم توجيه الصور في Angular: <img ngSrc> بدل <img src>. فالتوجيه NgOptimizedImage «يسهّل اعتماد أفضل ممارسات الأداء في تحميل الصور»: الصور غير المعلَّمة بـ priority تحصل افتراضياً على loading=lazy، وهو «يولّد srcset مناسباً» انطلاقاً من السمة sizes، ويشترط عرضاً وارتفاعاً، مما يمنع قفز التخطيط. علّم الصورة الرئيسية في الصفحة بـ priority، لأنها في الغالب عنصر LCP.
بيانات الميدان على أجهزة حقيقية. بيانات المختبر () تأتي من «بيئة مضبوطة بإعدادات محددة مسبقاً للجهاز والشبكة»، أما بيانات الميدان () فتأتي من مستخدمين حقيقيين، لذلك تعكس «الأجهزة الفعلية وظروف الشبكة». حاسوبك المحمول ليس حاسوب الفرع، ولا هاتف أندرويد القديم الذي يستخدمه زبائنك.
حاسوب المطوّر هو الجهاز الوحيد الذي لا يملكه مستخدموك أبداً.
ماذا تختار، ومتى
- نوع الميزانية: ابدأ بـ
initialوanyComponentStyle. وأضف ميزانية لكل حزمة حين تبدأ ميزة كسولة في التضخم. @deferأو مسار كسول: مسار كسول حين ينتمي الكود إلى شاشة أخرى؛ و@deferلعنصر واجهة ثقيل أسفل الشاشة الأولى () في الشاشة نفسها (رسم بياني، محرّر نصوص).- التمرير الافتراضي أو التقسيم إلى صفحات: التقسيم على الخادم حين يبحث المستخدمون ويصفّون النتائج؛ والتمرير الافتراضي حين يتصفحون فعلاً قائمة طويلة في صفحة واحدة.
- المختبر أو الميدان: في CI لاكتشاف التراجعات مبكراً، وبيانات الميدان لتقرير ما يجب إصلاحه.
في Atlas Bank
يحمّل Atlas Bank كل ميزاته تحميلاً كسولاً. ففي apps/atlas/src/app/app.routes.ts يوجد loadChildren واحد لكل قسم: لوحة المتابعة، والحسابات، والطلبات، والركنان القديمان كلاهما، فلا تكلّف شاشة شيئاً إلا حين يفتحها المستخدم.
والميزانيات في apps/atlas/project.json هي القيم الافتراضية لـ ، وقد أبقيتها عن قصد:
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
],يستخدم البناء إعدادات الإنتاج افتراضياً، ويشغّل CI في المستودع أمر build في كل طلب دمج ()، فتُفحص الميزانية قبل الدمج (). وتستخدم القوائم @for مع track t.id، ومنها جدول المعاملات في libs/feature/accounts/src/lib/account-detail.ts.
ما لا يملكه Atlas بعد: قائمة معاملات طويلة. ستكون إضافة صغيرة لتمرين على التمرير الافتراضي؛ أما اليوم فبيانات العرض التجريبي لا تضم إلا خمس عشرة معاملة.
ما تركته جانباً، ولماذا
- هزّ الشجرة ()، وتصغير الكود ()، والضغط. البناء يتكفّل بها.
- عمّال الويب (). أداة حقيقية، لكن العمل الثقيل في تطبيقات المؤسسات مكانه الخادم في أغلب الأحيان، لا عامل في المتصفح.
- و و . سطر واحد: استخدمها حين يُظهر القياس أن مورداً ما يصل متأخراً، لا بشكل افتراضي.
- صيغ الصور وشبكات للصور. يعمل
NgOptimizedImageمع مُحمِّل صور (image loader) حين يكون لديك CDN؛ واختيار الصيغ قرار تشغيلي.
الميزانية التي تُفشل البناء تحمي التطبيق من كل الفرق، وفريقك منها.
مع بداية الأسبوع
- افتح
angular.jsonأوproject.json. إذا لم يكن فيه قسمbudgets، أو كان حد الخطأ أعلى بكثير من الحجم الحالي، فاضبطه فوق حجم الحزمة الأولية الحالي بقليل. - اختر الشاشة التي ينقر عليها مستخدموك أكثر من غيرها، وانظر إلى قيمة INP الخاصة بها في بيانات الميدان، إن كنت تجمعها. وإن لم تكن تفعل، فهذه أول تذكرة ().
- ابحث عن أكبر
@forأو.map()في التطبيق، وتحقّق مما يتتبّعه.
لوكيل البرمجة الخاص بك
قواعد هذا المقال، مكتوبة لوكيل. الصقها في ملف AGENTS.md لديك، أو احفظها مهارةً ()، أو وجّه وكيلك إلى الملف.
# Frontend performance rules for enterprise apps Source: https://tt-tech.io/blog/frontend-performance-enterprise-budgets For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ. ## Rules 1. Keep a `budgets` section in the build configuration (`angular.json` or `project.json`) with `maximumWarning` and `maximumError`. Start with `initial` and `anyComponentStyle`. Add per-bundle budgets when a lazy feature starts to grow. 2. When there is no budget, or the error limit is far above today's size, set it just above the current initial bundle. 3. Load each feature through its own lazy route. Code that belongs to another screen goes in a lazy route; a heavy widget below the fold on the same screen (a chart, an editor) goes in `@defer`. In React, use `lazy` with a `<Suspense>` fallback. 4. Track every `@for` by a stable id (`track item.id`), never by object reference. Give React lists rendered with `.map()` a stable key in the same way. 5. For long lists, paginate on the server when users search and filter. Use virtual scroll (`<cdk-virtual-scroll-viewport>`) when they really scan a long list on one page. Virtual scroll fixes rendering, not downloading. 6. In image-heavy Angular screens, use `NgOptimizedImage` (`<img ngSrc>`, not `<img src>`) with a width and height, and `sizes` for the `srcset`. Mark the main image of the page `priority`. 7. Judge Core Web Vitals at the 75th percentile of page loads, mobile and desktop apart, never by average. Good is LCP within 2.5 seconds, INP at 200 milliseconds or less, CLS at 0.1 or less. 8. On data-heavy screens (filters, sorting, opening a row), watch INP first. 9. Use lab data (Lighthouse in CI) to catch regressions and field data from real users to decide what to fix. A run on a developer machine is not what users get. 10. Add `preload`, `prefetch` or `preconnect` only when a measurement shows a resource arrives late, not by default. ## Before you say done - Run the production build. It must pass the budgets. Report any new budget warning and the initial bundle size before and after. - Search the diff for `@for` blocks that track by object or `$index`, and for `.map()` lists without a stable key. - Search the diff for `<img src` in Angular templates, and for heavy libraries imported outside a lazy route or `@defer`.
المصادر
- web.dev, Web Vitals
- web.dev, Interaction to Next Paint (INP)
- web.dev, Performance budgets 101
- Angular, Building Angular apps
- Angular, Deferred loading with @defer
- React, lazy
- Angular CDK, Scrolling
- Angular, Control flow
- Angular, Optimizing images with NgOptimizedImage
- web.dev, Why lab and field data can be different (and what to do about it)