Développement Web

Angular control flow و @defer: إتقان الرسم المُؤجَّل

3 min de lecture

تغيّر محرّك القوالب في Angular بعمق منذ الإصدار 17. التوجيهات البنيويّة القديمة *ngIf و *ngFor و *ngSwitch لا تزال متعايشة مع الـ blocks الجديدة @if و @for و @switch، لكنّ النحو الجديد هو الذي يقود قرارات المعمارية من الآن فصاعداً. هذا الدرس يفصّل عمليّة الهجرة ويُقدّم خصوصاً الـ block @defer، الأداة التي تقسّم الـ bundle إلى قطع تُحمَّل عند الطلب بحسب مُحرّكات دقيقة (idle، viewport، interaction، hover، timer). في نهاية هذا المسار خطوة بخطوة، يصبح لديك project حقيقيّ يُحمّل مكوّناته الثقيلة فقط حين يحتاجها المستخدم.

المتطلّبات

  • Node.js 20 LTS أو أحدث، و Angular CLI مُثبَّت (npm install -g @angular/cli).
  • مشروع Angular 17 كحدّ أدنى (Angular 20 أو 21 موصى به لاستقرار التحكّم في التدفّق والـ hydration التزايديّ).
  • معرفة بمكوّنات standalone: إذا كانت modulesك تستعمل NgModule بعد، خطّط للتحوّل نحو standalone قبل الاستفادة الكاملة من @defer.
  • حوالي 45 دقيقة لمتابعة كامل الدرس وتنفيذ كلّ أمر.

الخطوة 1 — تحديث Angular وهجرة النحو

قبل كتابة أيّ سطر كود، لا بدّ من التأكّد أنّ إصدار الإطار يدعم التحكّم في التدفّق الجديد كنحو افتراضيّ. الـ block @if وصل في preview للمطوّرين في Angular 17 ثمّ استقرّ. منذ Angular 20، يأخذ الأولويّة على التوجيهات التاريخيّة، والـ CLI يقترح schematic هجرة آلية. هذه الهجرة آمنة لأنّها تحفظ الدلالة: *ngIf="user as u" يصبح @if (user; as u) ويحتفظ بالـ alias في نطاق الـ block.

# تحديث المشروع إلى الإصدار الهدف
ng update @angular/core @angular/cli

# تشغيل هجرة التحكّم في التدفّق على كامل الكود
ng generate @angular/core:control-flow

مُخرَج الـ console يُعدّد كلّ ملفّ مُتأثّر مع عدد التحويلات. لمشروع متوسّط الحجم (50 مكوّناً تقريباً)، احسب بضع ثوانٍ للتنفيذ. في النهاية، افتح أحد قوالبك لتعاين النتيجة: *ngIf اختفت، والقراءة تصبح أكثر طبيعيّة. إذا تركت الهجرة حالات لم تُحوَّل، يُدرَج تعليق // TODO: control flow للإشارة إلى منطق يدويّ يحتاج مراجعة.

الخطوة 2 — الشروط مع @if و @else

الـ block @if يحلّ محلّ *ngIf بقراءة أعلى ودعم أصيل لـ @else if. حيث كان النحو القديم يُجبر على سلسلة ثلاثة templates و ng-container، الـ block الجديد يُقرأ كـ if في JavaScript. الـ alias as يبقى متاحاً، ممّا يتجنّب التقييم المتعدّد لـ signal أو getter مُكلف.

@Component({
  selector: 'app-status',
  template: `
    @if (user(); as currentUser) {
      <p>مرحباً {{ currentUser.name }}.</p>
    } @else if (loading()) {
      <p>جارٍ تحميل ملفّك...</p>
    } @else {
      <p>لا مستخدم متّصل.</p>
    }
  `,
})
export class StatusComponent {
  user = inject(UserStore).current;
  loading = inject(UserStore).loading;
}

ثلاث فروع، ثلاث رسوم متمايزة، دون أيّ ng-template. لاحظ الاستدعاء user(): نعمل مع signal من Angular، استدعاؤه يُطلق القراءة التفاعليّة ويُسجّل المكوّن في رسم بياني للتبعيّات. الرسم يُحدَّث تلقائياً حين يتغيّر user. للتحقّق من نجاح التحويل، شغّل ng build في وضع التطوير ولاحظ غياب أيّ warning في الـ console: warning من نوع NG8003 (إشارة إلى توجيه دون exportAs أو import ناقص) يجب معالجته قبل المتابعة.

الخطوة 3 — الحلقات مع @for ومفتاح track الإلزاميّ

الـ block @for يجلب تغييراً كبيراً: الدالّة track لم تعد اختياريّة. Angular يرفض ترجمة حلقة لا تُحدّد كيف تُعرَّف عناصرها، وهذا خبر جيّد للأداء. بدون track، رسم قائمة من 200 سطر كان يُعيد إنشاء كلّ عُقدة DOM عند كلّ تحديث محتمل. مع مفتاح ثابت، فقط الإدخالات المُعدَّلة تُلامَس.

template: `
  <ul>
    @for (article of articles(); track article.id; let idx = $index, isLast = $last) {
      <li [class.last]="isLast">{{ idx + 1 }} — {{ article.title }}</li>
    } @empty {
      <li>لا مقالات متاحة.</li>
    }
  </ul>
`,

ثلاثة عناصر تستحقّ الانتباه. أوّلاً، track article.id يُشير لـ Angular كيف يربط بياناً بعُقدة DOM موجودة؛ إن تغيّر الترتيب أو أُدرج عنصر في وسط القائمة، يبقى الـ diff في أدنى حدّ. ثانياً، الـ block @empty يحلّ محلّ السلاسل القديمة من *ngIf="!list.length"، ممّا يجعل النيّة صريحة. أخيراً، المتغيّرات السياقيّة $index و $first و $last و $even و $odd و $count يمكن إعادة تسميتها بـ let لتجنّب تصادم الأسماء.

الخطوة 4 — التفرّع مع @switch

الـ block @switch يحلّ محلّ ngSwitch باستعمال مقارنة صارمة (===) وبدون fall-through ضمنيّ. هو الأداة المفضّلة لعرض views مختلفة بحسب حالة محدودة: حالة طلب، دور مستخدم، خطوة من onboarding. الـ block @default يلعب دور default في JavaScript ويلتقط كلّ القيم غير المذكورة صراحة.

template: `
  @switch (order().status) {
    @case ('pending')   { <app-pending /> }
    @case ('shipped')   { <app-shipped  /> }
    @case ('delivered') { <app-delivered /> }
    @default            { <app-unknown /> }
  }
`,

المُجمّع في Angular يفحص كلّ فرع وقت البناء ويُشير إلى القيم غير الصالحة حين يكون نوع order().status اتّحاداً (union) في TypeScript. هذا يسمح بضبط، أثناء الترجمة، حالة نَسِيتَ التعامل معها حين تُضيف مثلاً cancelled إلى النوع الأصلي. لتأكيد هذا السلوك، أضف مؤقّتاً cancelled إلى نوعك، احفظ، ولاحظ warning المُجمّع الذي يُشير إلى @switch غير المُكتمل.

الخطوة 5 — تقسيم الـ bundle مع @defer

الـ block @defer هو القطعة المركزيّة في هذا الدرس. دوره إزالة مكوّن — وكلّ شجرة تبعيّاته — من الـ bundle الرئيسيّ كي لا يُحمَّل إلّا في اللحظة المناسبة. تحصل على ملفّ JavaScript منفصل يُجلَب عند الطلب، ممّا يُخفّض بشكل جذريّ وزن أوّل رسم. مفيد خصوصاً لمكوّنات غنيّة: محرّر نصوص، خريطة تفاعليّة، رسم بيانيّ مع مكتبة ثقيلة، نموذج إدارة يراه قليل من المستخدمين.

template: `
  <h1>لوحة التحكّم</h1>
  <app-resume />

  @defer (on viewport) {
    <app-graphique-historique />
  } @placeholder (minimum 500ms) {
    <div class="skeleton">تحضير الرسم البياني...</div>
  } @loading (after 100ms; minimum 1s) {
    <app-spinner />
  } @error {
    <p>تعذّر تحميل هذه الوحدة.</p>
  }
`,

افحص الـ sub-blocks الأربعة. @placeholder يظهر قبل تحقّق شرط التحميل، مع مدّة عرض دنيا لتجنّب الوميض البصريّ. @loading يُعرَض أثناء الجلب الفعليّ للـ chunk، مع احترام تأخير ظهور (after 100ms يتجنّب إظهار spinner لتحميل سريع جدّاً). @error يحمي من خطأ شبكة أو فشل bundle. ترجم المشروع: ng build يُنتج الآن ملفّ chunk-XXXX.js منفصل لكلّ @defer مكتشَف، مرئيّ في تقرير البناء.

الخطوة 6 — اختيار المُحرّك المناسب

اختيار المُحرّك يُغيّر جذرياً التجربة المُدرَكة. Angular يقترح ستّة خيارات رئيسيّة: on idle، on viewport، on interaction، on hover، on immediate، و on timer(duration). كلّ واحد يستجيب لاستخدام مختلف. إليك مصفوفة القرار التي نطبّقها في الإنتاج.

المُحرّك متى يُستعمل مثال محسوس
on idle مكوّن مفيد لكن غير حرج عند الرسم الأوّل widget دردشة دعم في أسفل الصفحة
on viewport مكوّن ثقيل مرئيّ أبعد للأسفل خريطة تفاعليّة في ذيل مقال
on interaction مكوّن مُطلَق بنقرة أو focus modal تحرير، لوحة جانبيّة
on hover prefetch عند تمرير المؤشّر، تنفيذ على النقر قائمة معاينة، tooltip غنيّ
on immediate إخراج من الـ bundle دون تأخير العرض footer عامّ مُشترَك
on timer(2s) مكوّن ترويجيّ غير معرقِل شريط اشتراك في نشرة بريديّة

قاعدة بسيطة: إذا كان المكوّن مرئيّاً فوراً، لا تضعه في @defer. التأجيل ذو معنى فقط لما يمكنه الانتظار. لصفحة منتج، يمكن دمج منطقين بسلسلة المُحرّكات: @defer (on viewport; on idle) يُحمّل فور حدوث أحد الحدثين، ممّا يُغطّي المستخدم الذي يفعل scroll والذي يبقى ثابتاً.

الخطوة 7 — Préfetch لإخفاء الـ latency

على اتّصال بطيء، تنزيل chunk في لحظة النقر يُنتج انتظاراً محسوساً. التوجيه prefetch on يتجاوز هذه المشكلة بتنزيل الـ chunk قبل استعماله، دون تركيبه في الـ DOM. المتصفّح يحفظ الملفّ في cache HTTP وينفّذه فوراً حين يصل المُحرّك الحقيقيّ.

template: `
  @defer (on interaction; prefetch on hover) {
    <app-modal-edition [item]="selected()" />
  } @placeholder {
    <button (click)="open()">تعديل</button>
  }
`,

السيناريو واضح. المستخدم يمرّ بمؤشّره فوق الزرّ، الـ chunk يُنزَّل في الخلفيّة. بعد بضع مئات من الميلّي ثانية، ينقر: الـ module في الـ cache بالفعل، الـ modal يظهر دون أيّ تأخير. لقياس المكسب، افتح تبويب Network في المتصفّح، حاكِ اتّصال 3G بطيء، وقارن أوقات التحميل مع وبدون prefetch on hover. الفرق النموذجيّ من 300 إلى 600 ms على مكتبة مثل محرّر نصوص غنيّ.

الخطوة 8 — دمج @defer مع الـ hydration التزايديّ

حين يكون الرسم من الخادم فعّالاً، @defer يحاور الـ hydration التزايديّ المستقرّ في Angular 21. بدل إرسال HTML ساكن غير تفاعليّ، الخادم يُنتج الـ markup والمتصفّح يُحييه (hydrate) وفق نفس مُحرّكات @defer. هذا التضامن يُخفّض الوقت حتى التفاعل (TTI) دون التضحية بـ SEO.

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import {
  provideClientHydration,
  withIncrementalHydration,
} from '@angular/platform-browser';
import { App } from './app/app';

bootstrapApplication(App, {
  providers: [provideClientHydration(withIncrementalHydration())],
});

بعد تفعيل withIncrementalHydration()، يمكنك استبدال on viewport بـ hydrate on viewport في blocks @defer للاستفادة من الآليّة الجديدة. الـ HTML يُسلَّم مباشرة من الخادم، المستخدم يرى الصفحة في بضع ميلّي ثوان، وكلّ مكوّن يصبح تفاعليّاً فرديّاً حين يتحقّق مُحرّكه. لموقع تحريريّ بعشرة مكوّنات تفاعليّة مُبعثَرة في المقال، التوفير قد يتجاوز 40% على مقياس Time To Interactive.

الخطوة 9 — التحقّق من تقسيم الـ bundle

لا يحقّ لأيّ تحسين الثقة قبل قياسه. Angular CLI يُنتج تقرير حجم عند كلّ بناء، لكن يلزم بعض المنهجيّة لتفسير الأرقام. الأمر التالي يولّد تقرير JSON قابلاً للاستثمار من قِبَل أدوات تصوّر مثل source-map-explorer أو webpack-bundle-analyzer.

ng build --configuration production --stats-json
npx source-map-explorer dist/<mon-projet>/browser/main-*.js

الأداة تفتح treemap في المتصفّح. ابحث عن مكوّنك الثقيل: إذا ظهر في main-XXX.js، فهذا يعني أنّ @defer لم يأخذ مفعوله (غالباً بسبب import مباشر في مكوّن آخر يُجبره على البقاء في الـ bundle الرئيسي). إذا ظهر في chunk-YYYY.js، التقسيم يشتغل. معيار النجاح: bundle ابتدائيّ يحوي فقط ما يُعرَض فوق خطّ الطيّ.

الخطوة 10 — الاختبار في بيئة عرض نطاق محدود

الخطوة الأخيرة هي التحقّق من التجربة على اتّصال حقيقيّ. كثير من الجماهير تستعمل اتّصالات موبايل ذات سقف: القياس يجب أن يعكس هذا الواقع، لا الألياف المتوازنة. أدوات DevTools في Chrome و Firefox تُحاكي هذه الظروف بدقّة.

# Lighthouse من سطر الأوامر لتدقيق آليّ
npx lighthouse https://mon-site.example \
  --preset=desktop \
  --throttling.cpuSlowdownMultiplier=4 \
  --output=html \
  --output-path=./report.html

التقرير المُولَّد يُعدّد Web Vitals (LCP، INP، CLS) قبل وبعد التحسين. قبل وضع @defer في الإنتاج على مسار حرج، نفّذ التقرير مرّتين (مع وبدون التأجيل) لقياس المنفعة. على dashboard نموذجيّ بثلاثة widgets ثقيلة، نلاحظ بانتظام تحسناً في LCP من 800 ms إلى 1.2 s على اتّصال 3G محاكى.

أخطاء متكرّرة

الخطأ السبب الحلّ
الـ chunk لا يظهر في البناء المكوّن لا يزال مستورداً مباشرة في مكان آخر احذف الـ import الثابت، دع @defer يُدير التحميل
وميض spinner على اتّصال سريع لا عتبة after على @loading استعمل @loading (after 100ms; minimum 1s)
خطأ NG8003 بعد الهجرة إشارة إلى توجيه دون exportAs أو إلى module غير مستورد (مثلاً FormsModule منسيّ) أضف الـ import الناقص في imports الخاصّ بالمكوّن standalone، ثمّ أعد تشغيل خادم التطوير
الـ placeholder يبقى ظاهراً مكوّن مُصرَّح بأنّه non-standalone حوّله إلى standalone: true أو أَخرِجه من @defer
SSR يرسم HTML فارغاً @defer لا يُرسَم إلّا عند العميل افتراضياً فعّل withIncrementalHydration() واستعمل hydrate on

التكيّف مع الاتّصالات المحدودة

في أسواق حيث يبقى عرض نطاق الموبايل ثميناً، @defer ليس رفاهية بل ضرورة. تطبيق Angular غير مُحسَّن يعرض عادة bundle ابتدائي من 800 KB إلى 1.2 MB، ما يمثّل عدّة ثوان من التنزيل على 3G. باستخراج المكوّنات غير الحرجة (خريطة، إحصاءات تفصيليّة، modal إدارة)، نُعيد الـ bundle الابتدائيّ إلى 200-300 KB ونستردّ LCP تحت 2.5 s، وهي العتبة التي يعتبرها Google جيّدة. القاعدة العمليّة: كلّ مكوّن يستعمله أقلّ من 50% من الجلسات مرشّح للتأجيل.

أسئلة شائعة

هل يمكن دمج التوجيهات القديمة والتحكّم في التدفّق الجديد في نفس القالب؟
نعم تقنيّاً، لكنّه غير مُستحسَن. الخلط يُعقّد القراءة وبعض أدوات الهجرة لا تستطيع العمل تلقائياً. أفضل ترجمة قالب كاملاً دفعة واحدة بـ ng generate @angular/core:control-flow.

هل @defer يعمل بدون SSR؟
نعم، بل هذا هو استخدامه الرئيسيّ من جانب العميل. SSR و hydration تزايديّ يأتيان كزيادة حين تريد رسماً أوّليّاً لـ HTML ساكن وتقسيماً تفاعليّاً تدريجيّاً.

ما الفرق بين track و trackBy في *ngFor القديم؟
trackBy كان يطلب دالّة مُسمّاة مُصرَّحاً بها في المكوّن؛ track يقبل تعبيراً inline (نموذجياً track item.id)، ممّا يُلغي العقبة ويُحفّز على توفير مفتاح دائماً.

هل يُشارَك الـ chunk الخاصّ بـ @defer بين عدّة views؟
نعم، إذا أشار نفس المكوّن من قِبَل عدّة @defer في التطبيق، Angular يُحلّل الـ chunk لتجنّب التكرار. تشاهد هذا السلوك بوضوح في treemap الـ bundle analyzer.

هل يجب preFetch كلّ @defer؟
لا. الـ prefetch يستهلك عرض نطاق قبل الاستعمال. احتفظ به للمكوّنات المُرجَّحة على المدى القصير (تمرير المؤشّر، نيّة إجراء)؛ لمكوّن قليل الاستعمال، دع @defer يُحمّل عند الطلب.

للاستزادة

المراجع

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité