Адаптаванне служб Angular з большым колькасцю коду па запита за дапамою injectAsync
Чаму статычна ін’ектаваная служба включае своі бібліятэкі ў галоўны пакет Angular, і як injectAsync адкладае завантажэнне служб залежна ад намераў пользователя, уключаючы умовнае завантажэння.
Крыніцы: Stop Loading Massive Services: Mastering injectAsync in Angular 22
Рэжым атласнага завантажэння на рывень маршрута і блакі @defer не паводзяць сторонніях, якія вжываюцца рэдка, а таксама компаненты за межы пачатковага пакета дапрыглі Angular, але з сервісамі ўсё інакш. Одна-едынственная вызовка inject() для важкага сервіса ў компаненте, які завжды відображаецца, прыводзіць да таго, што гэты сервіс і ўсе бібліятекі, якія ён імпортуе, патрапляюць у галоўны частак пакета. У этай статыце пояснюецца, чаму так адбываецца, чаму старыя способы былі незручныя, і як API injectAsync, адкрыты ў Angular 22, дазволяе завантажваць код сервіса толькі тады, калі корыстнік практычна яго патрэбуе.
Рассіцьвачыце падробныя данні пра гэты API як момантальную інформацыю: на час напісання injectAsync ёсць новым, таму перад яго вжываннем паказвацеся неабходна ўпэўніцца ў яго точным сігнатурэ, статусе стабільнасці і правілах вжывання ў актуальной дакументацыі Angular.
Знайомая проблема з пакетам
Уявіце панель керування адміністрацыей, яка працюе эфектыва пад час роботы, але мае слабкі час першага завантажэння, калі пачатковы файл JavaScript вырашае да 2,4 МБ. Анаіз за дапамою tool’а source-map-explorer паказвае, што найбольшым вінуватцам зовсім не ўражоны элемент інтерфейсу. Це PdfExportService, які выкарыстоўвае важкія бібліятэкі для рендарування на стороне кліента, каб стварыць деталізаваныя багатосторонні фінансавыя звіты.
Цікава деталь — толькі апошнія 5% корыстувачаў калі-небудзь натыкаюцца на кнопку «Экспорт у PDF». Аднак, осколькі гэты сервіс вбудованы ў элемент заголовка панелі керування, кожны корыстучык завантажае пры кожнай першай візіце апошнія 600 КБ коду для экспорту. Цю цену платяць 95% корыстувачаў, якія ніколі не выкарыстоўваюць гэтую функцыю.
Angular уже прагне @defer і розбій коду на частины на адразе маршрутаў для компанентаў і сторанак. Але да сягодні ён не прагне чыстаг спосабу паслязаванняя загрузкі службы, заўсёды залічваючы яе ў системе внутршняга інжэкцыі залежнасцей (DI).
Чаму статычны метод inject() падае ў галоўны бандл
Ёжы бачыць, якія змены прыносіць injectAsync, корыстна зразумець, як такія інструменты для пакетавання, як esbuild і Vite, взаімаюцца з системай DI Angular. Разглянем компанент-загалоўку, які інжэктуе службу звятыкаў у звычны спосаб:
import { Component, inject } from '@angular/core';
import { HeavyReportService } from './heavy-report.service';
@Component({ ... })
export class DashboardHeaderComponent {
// The Static Analysis Trap
private reportService = inject(HeavyReportService);
exportData() {
this.reportService.generate();
}
}
Сама запізнювальна інструкцыя import на высшам рэвэлі ёсць тым, што мае значэнне. Инструмент для збору коду аналізуе імпорты статычна, бачыць, што DashboardHeaderComponent залежыць ад HeavyReportService, і размешчае гэты сервіс, а таксама кожную бібліятэку трэцьяго сторонньага вывару, якую ён імпортуе, у той жа фрагмент, што і сам компонент. Якщо компонент ўскладнівае пачатковы відобразэння, то і бібліятэка PDF таксама. Сам вызов inject() не ёсць проблемай; проблемай являецца статычны імпорт, які гэта можлівеяе.
Старыя способы рашэння проблемы і ўскладнення, якія вони ствараюць
Апытныя разработчыкі вядома час адмовляліся ад гэтага, пераходзячы на дынамічны await import(...) унутрь обрабоўчыка кліку, таким чынам заставляючы інструмент для збору коду выдаваць сервіс як околачны фрагмент.
Складна частка заключаецца ў тым, што клас, імпортаваны дынамічна, ўсё ж застаецца проста класам. Якщо вы стварылі яго самі, ён знаходзится па за межамы інжектара Angular, таму такія залежнасці, як HttpClient для запрашэння шаблона, або служба автаналізацыі, якая додае токен, не задаюцца. Ёжы правільна інстанцыя пагадвала таксама прымесце Injector з самага пачатку і разв’язваць завантажаны клас через яго, што працюе, але распростраńвае механізм DI ў код компонентаў і лёгка пашкодзіцца. (Ёжы перапамін пра тое, як сам разв’язваць працюе, адзірніце як Angular dependency injection разв’язвае службы за дапамогою inject.)
Што змініў injectAsync
injectAsync скасоўвае разлік межаў між дынамічным імпортам у ECMAScript і системай інжектавання залежнасцей у Angular. Вы описваеце залежнасць за дапамогою функцыі-завантажувача, і фактычна кажаеце інструменту пакетавання: не включайце гэта ў галоўны чак, а завантажыце і створыце яго толькі колі будзе запатрэбна.
Інжектаванне залежнасцей больш не павінна быць сінхронным. Завантажэнне коду службы можа чакаць да моменту, калі корыстнік падае заявку.
Шаг 1: напісаць службу як зазвычай
Нічога ў самай службе не трэба меняць. Яна все ўсё можа викорыстоўваць inject() і ўсё іншае з екосістэмы Angular. Хутчэй яна інжектуе HttpClient і імпортуе pdfMake — большую залежнасць, якую вы хочаце не включаць у пачатковы завантажэння.
// heavy-report.service.ts (This will become its own separate JS chunk)
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as pdfMake from 'pdfmake/build/pdfmake'; // Massive dependency
@Injectable({ providedIn: 'root' })
export class HeavyReportService {
private http = inject(HttpClient);
async generate(data: any) {
// Complex, heavy PDF generation logic
}
}
Паколькі нічога ў коде, які завантажваецца асьцярожна, не імпортуе гэты файл статычна, збіральнік можа выдаты яго, а таксама pdfMake, як окольны фрагмент.
Шаг 2: разв’язваць прыбутак на межы взаімадзеяння
У замест на вставкі прыбутка як поля класа, компонент запрашвае яго, калі натыкаюцца на кнопку экспорту. Сігнал isExporting керуе станам і пазначэнням кнопкі, паколькі фрагмент завантажваецца і виконваецца заданне.
// The Modern Code-Split Architecture
import { Component, injectAsync, signal } from '@angular/core';
@Component({
selector: 'app-dashboard-header',
template: `
<button (click)="onExport()" [disabled]="isExporting()">
{{ isExporting() ? 'Loading Exporter...' : 'Export PDF' }}
</button>
`
})
export class DashboardHeaderComponent {
isExporting = signal(false);
async onExport() {
this.isExporting.set(true);
try {
// 1. The Magic: The browser downloads the chunk over the network right now.
// Angular instantiates it and wires up all its DI dependencies (like HttpClient).
const reportService = await injectAsync(
() => import('./heavy-report.service').then(m => m.HeavyReportService)
);
// 2. Execute the heavy logic
await reportService.generate({ id: 123 });
} catch (error) {
console.error('Failed to load or execute the report service', error);
} finally {
this.isExporting.set(false);
}
}
}
Калі запускаецца працоўнік, браузер запрашвае частку службы, Angular стварае екземпляр і задае яму залежнасці, такія як HttpClient, а компонент вызывае generate(). Тут важлівы блакі try/catch/finally: адмова сеті пад завантажэнням часткі тепер можа стаць падзею часу выканання, а блак finally гарантуе, што кнопка будзе знову увімкнутая ў будзь-якім случае.
Одна деталь, яю трэба пераканацца ў даследжэннях: як і inject(), функцыі DI зазвычай павінны быць вызываныя ў контексте інжэкціі, напрыклад у конструктары или ініціялайзеры поля, а не ў будзь-якім працоўніку падзеі. Якщо ваша версія Angular вымагае гэтага для injectAsync, вызывайце яе ў ініціялайзеры поля, каб отрымаць загрузчык, і пасля таго вызывайце гэты загрузчык з працоўніка кліку.
Пасля стварэння версіі для выпуску выходны файл складаецца з невялікага файла main.js і адзінокаго чанка heavy-report.service-[hash].js. Карыстальнікі, які ніколі не клацаюць на кнопку экспорту, зовсама не завантажуюць бібліятэку PDF.
Умовна загрузка: выдаваецца толькі код, які можа запускаць карыстальнік
Раздзелэванне вялікай бібліятэкі — гэта яўны спосаб её использовання, але той жа механізм дазволяе рэалізаваць умовную інжектаванне. Падазроўваю, што безплатныя абліквыціі викорыстоўвають StandardDataProcessor, тады як прэміум-корпаратыўныя абліквыціі — AIProDataProcessor.
Традыцыйны падход заключаецца у збіранні обох версій і викорыстанні фабрычнага прадастальніка для выбору адной з іх у часе запуску, таму кожны карыстальнік завантажвае обе. За дапамогою injectAsync сама гілка вырашае, який чанк неабходна завантажыць:
async processData(payload: any) {
this.isProcessing.set(true);
let processor;
if (this.currentUser.tier === 'PRO') {
// Only Pro users pay the network cost for the AI chunk
processor = await injectAsync(
() => import('./ai-pro-processor.service').then(m => m.AiProProcessorService)
);
} else {
// Free users download the lightweight chunk
processor = await injectAsync(
() => import('./standard-processor.service').then(m => m.StandardProcessorService)
);
}
await processor.execute(payload);
this.isProcessing.set(false);
}
Професійныя корыстувальнікі плацуюць за выкарыстанне процэсара AI, а всі іншы аплекуюць лёгкі варыянт. Це ўзлетна поліморфізма, пры якой выбор таксама вялічыну таго, што завантажваецца, ўз’яднваючы розмер таго, што завантажае корыстувальнік, з абсолютным розмерам кодавой базы. У нынешняй версіі isProcessing застаецца true, калі ведзецца завантажэнне, або калі execute() выклекае аднойчыну; трэба загорнуць тэла коду ў блокі try/finally, як у прыкладзе экспорту. Наданне обох процэсараў спяльнага інтэрфейсу таксама дапамагае правільна задаць тип processor, а не застаўляць яго імпліцытна типам any.
Калі не трэба яго вжываць
Леніва загрузка прагне зменшыць пачатковы размер пакета за рахунак адкладэння загрузкі ў момент выкарыстоўвання. Для службы, якой большасць выкарыстоўнікаў патрабуе ў кожны раз, як толькі яны заходзяць, адкладэння загрузкі проста пераносіць чаканне на горшы момент. Це мае сэнс для важкіх, рэдка выкарыстоўваных або спецыфічных для певных рэжымоў функцый. Таксама можна зменшыць адкладэння, запускаючы загрузку пад час наведення курсора або акцэнтавання на кнопцы, прычаму ўжо перш чым яе нажмаць.
Ключовыя выводы
- Спачатку адбавьце аудыт: выкарыстоўваючы source-map-explorer або падобны інструмент, знайдзіце важкія службы, якія выкарыстоўваны толькі рэдка або за певныя умовы.
- Адключыце статычныя ін’екціі гэтых служб з компонентаў, якія загружаюцца адразу, адкуль ўсё-такі вядуць яны ў галоўны пакет.
- Загружайце іх у момент намеру выкарыстоўніка за дапамогай
injectAsyncі дынамічнагаimport(), пры чым захавайце падтрымку полной інжектавання залежнасцяў.