วิธีปรับปรุงประสิทธิภาพของแอป 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 ใช่ไหม ติดต่อเราและเลือกบริการพัฒนาแอปเชิงมุมของเรา