วิธีปรับปรุงประสิทธิภาพของแอป Angular ของคุณ

เผยแพร่แล้ว: 2020-04-10

เมื่อพูดถึงเฟรมเวิร์กส่วนหน้าที่ยอดเยี่ยมที่สุด เป็นไปไม่ได้ที่จะไม่พูดถึง Angular ต้องใช้ความพยายามอย่างมากจากโปรแกรมเมอร์ในการเรียนรู้และใช้งานอย่างชาญฉลาด น่าเสียดายที่มีความเสี่ยงที่นักพัฒนาที่ไม่มีประสบการณ์ใน Angular สามารถใช้คุณลักษณะบางอย่างในลักษณะที่ไม่มีประสิทธิภาพ

สิ่งหนึ่งที่คุณต้องทำงานอยู่เสมอในฐานะนักพัฒนาส่วนหน้าคือประสิทธิภาพของแอป โปรเจ็กต์ที่ผ่านมาส่วนใหญ่ของฉันมุ่งเน้นไปที่แอปพลิเคชันระดับองค์กรขนาดใหญ่ที่มีการขยายและพัฒนาอย่างต่อเนื่อง กรอบงานส่วนหน้าจะมีประโยชน์อย่างยิ่งที่นี่ แต่สิ่งสำคัญคือต้องใช้อย่างถูกต้องและสมเหตุสมผล

ฉันได้เตรียมรายการอย่างรวดเร็วของกลยุทธ์และเคล็ดลับการเพิ่มประสิทธิภาพที่ได้รับความนิยมมากที่สุดที่อาจช่วยให้คุณ เพิ่มประสิทธิภาพของแอปพลิเคชัน Angular ของคุณ ได้ทันที โปรดทราบว่าคำแนะนำทั้งหมดที่นี่ใช้กับ Angular ในเวอร์ชัน 8

ChangeDetectionStrategy และ ChangeDetectorRef

Change Detection (CD) เป็นกลไกของ Angular สำหรับตรวจจับการเปลี่ยนแปลงข้อมูลและตอบสนองต่อการเปลี่ยนแปลงโดยอัตโนมัติ เราสามารถแสดงรายการการเปลี่ยนแปลงสถานะแอปพลิเคชันมาตรฐานประเภทพื้นฐานได้:

  • กิจกรรม
  • คำขอ HTTP
  • ตัวจับเวลา

สิ่งเหล่านี้เป็นการโต้ตอบแบบอะซิงโครนัส คำถามคือ Angular จะทราบได้อย่างไรว่ามีการโต้ตอบบางอย่าง (เช่น การคลิก ช่วงเวลา คำขอ http) และจำเป็นต้องอัปเดตสถานะแอปพลิเคชัน

คำตอบคือ ngZone ซึ่งโดยพื้นฐานแล้วเป็นระบบที่ซับซ้อนซึ่งหมายถึงการติดตามการโต้ตอบแบบอะซิงโครนัส หากการดำเนินการทั้งหมดลงทะเบียนโดย ngZone Angular จะรู้ว่าเมื่อใดควรตอบสนองต่อการเปลี่ยนแปลงบางอย่าง แต่ไม่รู้ว่ามีอะไรเปลี่ยนแปลงไปบ้าง และเปิดใช้กลไก Change Detection ซึ่งจะตรวจสอบส่วนประกอบทั้งหมดตามลำดับในเชิงลึก

แต่ละองค์ประกอบในแอป Angular มีตัวตรวจจับการเปลี่ยนแปลงของตัวเอง ซึ่งกำหนดว่าองค์ประกอบนี้ควรทำงานอย่างไรเมื่อมีการเปิดใช้การตรวจจับการเปลี่ยนแปลง ตัวอย่างเช่น หากจำเป็นต้องแสดงผล DOM ของส่วนประกอบอีกครั้ง (ซึ่งค่อนข้างเป็นการดำเนินการที่มีราคาแพง) เมื่อ Angular เปิดตัว Change Detection ทุกองค์ประกอบจะถูกตรวจสอบและมุมมอง (DOM) อาจแสดงผลใหม่ตามค่าเริ่มต้น

เราสามารถหลีกเลี่ยงสิ่งนี้ได้โดยใช้ ChangeDetectionStrategy.OnPush:

 @ส่วนประกอบ({
  ตัวเลือก: 'foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

ดังที่คุณเห็นในโค้ดตัวอย่างด้านบน เราต้องเพิ่มพารามิเตอร์เพิ่มเติมให้กับมัณฑนากรของส่วนประกอบ แต่กลยุทธ์การตรวจจับการเปลี่ยนแปลงใหม่นี้ทำงานอย่างไร

กลยุทธ์บอก Angular ว่าส่วนประกอบเฉพาะขึ้นอยู่กับ @Inputs() ของมันเท่านั้น นอกจากนี้ ส่วนประกอบทั้งหมด @Inputs() จะทำหน้าที่เหมือนวัตถุที่ ไม่เปลี่ยนรูป (เช่น เมื่อเราเปลี่ยนเฉพาะคุณสมบัติใน @Input() ของวัตถุ โดยไม่เปลี่ยนการอ้างอิง ส่วนประกอบนี้จะไม่ถูกตรวจสอบ หมายความว่าจะละเว้นการตรวจสอบที่ไม่จำเป็นจำนวนมากและควรเพิ่มประสิทธิภาพแอปของเรา

ส่วนประกอบที่มี ChangeDetectionStrategy.OnPush จะถูกตรวจสอบเฉพาะในกรณีต่อไปนี้:

  • @Input() การอ้างอิงจะเปลี่ยน
  • เหตุการณ์จะถูกทริกเกอร์ในเทมเพลตของคอมโพเนนต์หรือรายการย่อยตัวใดตัวหนึ่ง
  • สังเกตได้ในองค์ประกอบจะทำให้เกิดเหตุการณ์
  • ซีดี จะถูกเรียกใช้ด้วยตนเองโดยใช้บริการ ChangeDetectorRef
  • ใช้ ไพพ์ async ในมุมมอง (ไพพ์ async ทำเครื่องหมายส่วนประกอบที่จะตรวจสอบการเปลี่ยนแปลง – เมื่อสตรีมต้นทางจะปล่อยค่าใหม่ส่วนประกอบนี้จะถูกตรวจสอบ)

หากไม่มีสิ่งใดข้างต้นเกิดขึ้น การใช้ ChangeDetectionStrategy.OnPush ภายในส่วนประกอบเฉพาะจะทำให้ส่วนประกอบและส่วนประกอบที่ซ้อนกันทั้งหมดไม่ถูกตรวจสอบหลังจากเปิดตัวซีดี

โชคดีที่เรายังคงสามารถควบคุมการตอบสนองต่อการเปลี่ยนแปลงข้อมูลได้อย่างเต็มที่โดยใช้บริการ ChangeDetectorRef เราต้องจำไว้ว่าด้วย ChangeDetectionStrategy.OnPush ภายในระยะหมดเวลา คำขอ การบอกรับสมาชิก เราจำเป็นต้องเริ่มการทำงานของ CD ด้วยตนเอง หากเราต้องการสิ่งนี้จริงๆ:

 ตัวนับ = 0;

ตัวสร้าง (ส่วนตัว changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
  setTimeout(() => {
    this.counter += 1,000;
    this.changeDetectorRef.detectChanges();
  }, 1,000);
}

ดังที่เราเห็นข้างต้น โดยการเรียก this.changeDetectorRef.detectChanges() ภายในฟังก์ชัน timeout ของเรา เราสามารถบังคับ CD ได้ด้วยตนเอง หากมีการใช้ตัวนับภายในเทมเพลตไม่ว่าในทางใด ค่าของตัวนับจะถูกรีเฟรช

เคล็ดลับสุดท้ายในส่วนนี้เกี่ยวกับการปิดใช้งาน ซีดี อย่างถาวรสำหรับส่วนประกอบเฉพาะ หากเรามีองค์ประกอบแบบคงที่และเรามั่นใจว่าไม่ควรเปลี่ยนสถานะขององค์ประกอบนั้น เราสามารถปิดใช้งาน ซีดี อย่างถาวรได้:

 this.changeDetectorRef.detach()

โค้ดนี้ควรดำเนินการภายในวิธีวงจรชีวิต ngAfterViewInit() หรือ ngAfterViewChecked() เพื่อให้แน่ใจว่ามุมมองของเราแสดงผลอย่างถูกต้องก่อนที่เราจะปิดใช้งานการรีเฟรชข้อมูล คอมโพเนนต์นี้จะไม่ถูกตรวจสอบระหว่าง ซีดี อีกต่อไป เว้นแต่เราจะทริกเกอร์ detectChanges() ด้วยตนเอง

เรียกใช้ฟังก์ชันและตัวรับในเทมเพลต

การใช้การเรียกใช้ฟังก์ชันภายในเทมเพลตจะเรียกใช้ฟังก์ชันนี้ทุกครั้งที่เรียกใช้ Change Detector สถานการณ์เดียวกันนี้เกิดขึ้นกับ getters ถ้าเป็นไปได้เราควรพยายามหลีกเลี่ยงสิ่งนี้ ในกรณีส่วนใหญ่ เราไม่จำเป็นต้องรันฟังก์ชันใดๆ ภายในเทมเพลตของคอมโพเนนต์ระหว่างรัน ซีดี ทุกครั้ง แทนที่จะใช้ท่อบริสุทธิ์

ท่อบริสุทธิ์

ท่อบริสุทธิ์เป็นท่อ ชนิดหนึ่งที่มีเอาต์พุตซึ่งขึ้นอยู่กับอินพุตเท่านั้นโดยไม่มีผลข้างเคียง โชคดีที่ไพพ์ทั้งหมดใน Angular นั้นบริสุทธิ์โดยค่าเริ่มต้น

 @ท่อ({
    ชื่อ: 'ตัวพิมพ์ใหญ่',
    บริสุทธิ์: จริง
})

แต่ทำไมเราควรหลีกเลี่ยงการใช้ท่อที่มี pure: false? คำตอบคือ Change Detection อีกครั้ง ไพพ์ที่ไม่บริสุทธิ์จะถูกดำเนินการในการรันซีดีทุกครั้ง ซึ่งไม่จำเป็นในกรณีส่วนใหญ่ และทำให้ประสิทธิภาพของแอปลดลง นี่คือตัวอย่างของฟังก์ชันที่เราสามารถเปลี่ยนเป็นไพพ์บริสุทธิ์ได้:

 แปลง (ค่า: สตริง จำกัด = 60 จุดไข่ปลา = '...') {
  ถ้า (!value || value.length <= จำกัด) {
    ส่งกลับค่า;
  }
  const numberOfVisibleCharacters = value.substr(0, จำกัด).lastIndexOf(' ');
  คืนค่า `${value.substr(0, numberOfVisibleCharacters)}${ellipsis}`;
}

และมาดูมุมมอง:

 <p class="description">ตัดทอน(ข้อความ, 30)</p>

โค้ดด้านบนแสดงถึงฟังก์ชันล้วนๆ ไม่มีผลข้างเคียง เอาต์พุตขึ้นอยู่กับอินพุตเท่านั้น ในกรณีนี้ เราสามารถแทนที่ฟังก์ชันนี้ด้วย pure pipe :

 @ท่อ({
  ชื่อ: 'ตัดทอน',
  บริสุทธิ์: จริง
})
คลาสการส่งออก TruncatePipe ใช้ PipeTransform {
  แปลง (ค่า: สตริง จำกัด = 60 จุดไข่ปลา = '...') {
    ...
  }
}

และสุดท้าย ในมุมมองนี้ เราได้รับโค้ด ซึ่งจะดำเนินการก็ต่อเมื่อข้อความถูกเปลี่ยน โดยไม่ขึ้นกับ Change Detection

 <p class="description">{{ ข้อความ | ตัดทอน: 30 }}</p>

โมดูลโหลดและโหลดล่วงหน้าแบบขี้เกียจ

เมื่อแอปพลิเคชันของคุณมีมากกว่าหนึ่งหน้า คุณควรพิจารณาสร้างโมดูลสำหรับแต่ละส่วนเชิงตรรกะของโครงการของคุณโดยเฉพาะอย่างยิ่ง โมดูลการโหลดแบบ Lazy Loading Module ลองพิจารณารหัสเราเตอร์เชิงมุมอย่างง่าย:

 เส้นทาง const: เส้นทาง = [
  {
    เส้นทาง: '',
    ส่วนประกอบ: HomeComponent
  },
  {
    เส้นทาง: 'foo',
    loadChildren: ()=> นำเข้า ("./foo/foo.module") จากนั้น (m => m.FooModule)
  },
  {
    เส้นทาง: 'บาร์',
    loadChildren: ()=> นำเข้า ("./bar/bar.module") จากนั้น (m => m.BarModule)
  }
]
@NgModule({
  การส่งออก: [โมดูลเราเตอร์],
  การนำเข้า: [RouterModule.forRoot(เส้นทาง)]
})
คลาส AppRoutingModule {}

ในตัวอย่างด้านบน เราจะเห็นว่า fooModule ที่มีเนื้อหาทั้งหมดจะถูกโหลดก็ต่อเมื่อผู้ใช้พยายามป้อน เส้นทาง เฉพาะ (foo หรือ bar) Angular จะสร้างส่วนแยก ต่างหาก สำหรับ โมดูล นี้ การโหลดแบบ Lazy จะช่วยลดการ โหลดเริ่มต้น

เราสามารถเพิ่มประสิทธิภาพเพิ่มเติมได้ สมมติว่าเราต้องการสร้าง โมดูล การโหลดแอปในพื้นหลัง สำหรับกรณีนี้ เราสามารถใช้ preloadingStrategy โดยค่าเริ่มต้น Angular จะมี preloadingStrategy สองประเภท:

  • ไม่มีการโหลดล่วงหน้า
  • พรีโหลดโมดูลทั้งหมด

ในโค้ดด้านบนจะใช้กลยุทธ์ NoPreloading เป็นค่าเริ่มต้น แอปเริ่มโหลดโมดูลเฉพาะตามคำขอของผู้ใช้ (เมื่อผู้ใช้ต้องการดูเส้นทางเฉพาะ) เราสามารถเปลี่ยนสิ่งนี้ได้โดยเพิ่มการกำหนดค่าพิเศษบางอย่างให้กับเราเตอร์

 @NgModule({
  การส่งออก: [โมดูลเราเตอร์],
  การนำเข้า: [RouterModule.forRoot (เส้นทาง, {
       preloadingStrategy: PreloadAllModules
  }]
})
คลาส AppRoutingModule {}

การกำหนดค่านี้ทำให้เส้นทางปัจจุบันแสดงโดยเร็วที่สุด และหลังจากนั้น แอปพลิเคชันจะพยายามโหลดโมดูลอื่นๆ ในพื้นหลัง ฉลาดไม่ใช่เหรอ? แต่นั่นไม่ใช่ทั้งหมด หากโซลูชันนี้ไม่ตรงกับความต้องการของเรา เราก็สามารถเขียน กลยุทธ์ที่เรากำหนดเอง ได้

สมมติว่าเราต้องการโหลดเฉพาะโมดูลที่เลือกไว้ล่วงหน้าเท่านั้น เช่น BarModule เราระบุสิ่งนี้โดยการเพิ่มฟิลด์พิเศษสำหรับฟิลด์ข้อมูล

 เส้นทาง const: เส้นทาง = [
  {
    เส้นทาง: '',
    ส่วนประกอบ: HomeComponent
    ข้อมูล: { พรีโหลด: เท็จ }
  },
  {
    เส้นทาง: 'foo',
    loadChildren: ()=> นำเข้า ("./foo/foo.module") จากนั้น (m => m.FooModule),
    ข้อมูล: { พรีโหลด: เท็จ }
  },
  {
    เส้นทาง: 'บาร์',
    loadChildren: ()=> นำเข้า ("./bar/bar.module") จากนั้น (m => m.BarModule),
    ข้อมูล: { พรีโหลด: จริง }
  }
]

จากนั้นเราต้องเขียนฟังก์ชันพรีโหลดแบบกำหนดเองของเรา:

 @ฉีดได้()
คลาสการส่งออก CustomPreloadingStrategy ใช้ PreloadingStrategy {
  โหลดล่วงหน้า (เส้นทาง: เส้นทาง, โหลด: () => สังเกตได้<ใด ๆ>): สังเกตได้<ใด ๆ> {
    ส่งคืน route.data && route.data.preload ? load() : ของ (null);
  }
}

และตั้งเป็น preloadingStrategy:

 @NgModule({
  การส่งออก: [โมดูลเราเตอร์],
  การนำเข้า: [RouterModule.forRoot (เส้นทาง, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
คลาส AppRoutingModule {}

สำหรับตอนนี้ เฉพาะ เส้นทาง ที่มีพารามิเตอร์ { data: { preload: true } } เท่านั้นที่จะถูกโหลดล่วงหน้า เส้นทาง ที่เหลือจะทำหน้าที่เหมือนไม่มีการตั้งค่า NoPreloading

PreloadingStrategy แบบกำหนดเองคือ @Injectable() ซึ่งหมายความว่าเราสามารถฉีดบริการบางอย่างเข้าไปภายในได้ หากเราต้องการและปรับแต่ง preloadingStrategy ด้วยวิธีอื่นใด
ด้วย เครื่องมือสำหรับนักพัฒนาของเบราว์เซอร์ เราสามารถตรวจสอบการเพิ่มประสิทธิภาพด้วย เวลาโหลดเริ่มต้นที่ เท่ากันโดยมีและไม่มี preloadingStrategy นอกจากนี้เรายังสามารถดูแท็บเครือข่ายเพื่อดูว่าชิ้นส่วนสำหรับเส้นทางอื่นกำลังโหลดอยู่ในพื้นหลัง ในขณะที่ผู้ใช้สามารถดูหน้าปัจจุบันได้โดยไม่ชักช้า

ฟังก์ชัน trackBy

เราสามารถสรุปได้ว่าแอปเชิงมุมส่วนใหญ่ใช้ *ngFor เพื่อวนซ้ำรายการที่อยู่ในเทมเพลต หากรายการที่ซ้ำกันสามารถแก้ไขได้ trackBy เป็นสิ่งที่ต้องมีอย่างแน่นอน

 <ul>
  <tr *ngFor="let product of products; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId (ดัชนี: หมายเลข ผลิตภัณฑ์: ผลิตภัณฑ์) {
  ส่งคืน product.id;
}

โดยใช้ฟังก์ชัน trackBy Angular สามารถติดตามว่าองค์ประกอบใดของคอลเลกชันที่มีการเปลี่ยนแปลง (โดยตัวระบุที่กำหนด) และแสดงผลเฉพาะองค์ประกอบเฉพาะเหล่านี้อีกครั้ง เมื่อเราละเว้น trackBy รายการทั้งหมดจะถูกโหลดใหม่ซึ่งอาจเป็นการดำเนินการที่ใช้ทรัพยากรมากบน DOM

การรวบรวมล่วงหน้า (ทอท.)

เกี่ยวกับเอกสารเชิงมุม:

(…) ส่วนประกอบและเทมเพลตที่จัดทำโดย Angular ไม่สามารถเข้าใจโดยเบราว์เซอร์โดยตรง แอปพลิเคชันเชิงมุมต้องการกระบวนการรวบรวมก่อนที่จะสามารถทำงานในเบราว์เซอร์

Angular จัดเตรียมการคอมไพล์สองประเภท:

  • Just-in-Time (JIT) – รวบรวมแอพในเบราว์เซอร์ที่รันไทม์
  • Ahead-of-Time (ทอท.) – รวบรวมแอพในเวลาบิลด์

สำหรับการใช้งานการพัฒนา การรวบรวม JIT ควรครอบคลุมความต้องการของนักพัฒนา อย่างไรก็ตาม สำหรับการสร้างการผลิต เราควรใช้ ทอท. อย่างแน่นอน เราจำเป็นต้องตรวจสอบให้แน่ใจว่าตั้งค่าสถานะ aot ภายในไฟล์ angular.json เป็นจริง ประโยชน์ที่สำคัญที่สุดของโซลูชันดังกล่าว ได้แก่ การเรนเดอร์ที่เร็วขึ้น คำขอแบบอะซิงโครนัสน้อยลง ขนาดการดาวน์โหลดเฟรมเวิร์กที่เล็กลง และการรักษาความปลอดภัยที่เพิ่มขึ้น

สรุป

ประสิทธิภาพของแอปพลิเคชันเป็นสิ่งที่คุณต้องคำนึงถึงทั้งในระหว่างการพัฒนาและส่วนการบำรุงรักษาของโครงการของคุณ อย่างไรก็ตาม การค้นหาวิธีแก้ไขที่เป็นไปได้ด้วยตนเองอาจใช้เวลาและความพยายามอย่างมาก การตรวจสอบข้อผิดพลาดที่มักเกิดขึ้นบ่อยๆ และจดจำไว้ในระหว่างขั้นตอนการพัฒนา ไม่เพียงแต่ช่วยให้คุณปรับปรุงประสิทธิภาพของแอป Angular ได้ในเวลาไม่นาน แต่ยังช่วยให้คุณหลีกเลี่ยงความเหลื่อมล้ำในอนาคตได้อีกด้วย

ปล่อยไอคอนผลิตภัณฑ์

ไว้วางใจ Miquido กับโครงการเชิงมุมของคุณ

ติดต่อเรา

ต้องการพัฒนาแอพด้วย Miquido หรือไม่?

กำลังคิดที่จะเพิ่มประสิทธิภาพให้กับธุรกิจของคุณด้วยแอป Angular ใช่ไหม ติดต่อเราและเลือกบริการพัฒนาแอปเชิงมุมของเรา