Уже сложно представить наши приложения без такой оптимизации, как tree shaking.
Tree-shaking — «встряхивание дерева», удаление неиспользуемого кода из бандла приложения во время сборки.

Почему же я хочу уделить особое внимание standalone компонентам?

Просто существуют некоторые нюансы при встряхивании, о которых стоит знать при работе с такими компонентами. Как говорится, предупрежден — значит вооружен.
Далее можете почитать немного теории или сразу перейти к основной мысли.

Давным-давно...

История tree-shaking в Angular интересна.

  • До 6 версии фреймворка была возможность указать провайдеры только на уровне модуля в самом модуле и на уровне компонента/директивы.

  • С 6 версии появляются tree-shakable провайдеры: указываются через Injectable декоратор + provideIn в самом сервисе.

    @Injectable({ providedIn: TestModule })
    export class TestApiService {...

    Теперь модулю необязательно объявлять сервис у себя в providers. Соответственно, больше нет ссылки на этот сервис => встряхивание дерева отработает ожидаемо: если сервис нигде не используется, то он не будет включен в финальный бандл.

  • Затем приходит новый движок рендеринга Ivy (до этого был View Engine), который использует концепцию incremental dom и улучшает возможности tree-shaking. Теперь для каждого компонента имеются инструкции по созданию/обновлению DOM-дерева, что позволяет встряхивать еще больше кода в процессе tree-shaking.

В общем, tree-shaking есть и он работает. Стоит только помнить, что указывая провайдеры в providers модуля, мы лишаем себя возможности встряхнуть такие сервисы во время билда.

Standalone компоненты

В Angular 14 выпускают standalone components, которые больше не нужно объявлять в модулях. Стоит только добавить standalone: true флаг.

Standalone компоненты отличаются явным подключением зависимостей внутри себя: в этом плане они ведут себя как модули. Они могут импортировать в себя и модули, и другие standalone компоненты и сами быть импортированы в модуль или standalone компонент.
У таких компонентов достаточно плюшек (меньше кода, directive composition api, тестирование, сторибук и так далее), но это уже тема отдельной статьи.

Standalone компоненты и tree-shaking

И вот мы начинаем активно создавать standalone компоненты, импортировать их то в модули, то в такие же компоненты. И однажды замечаем, что production bundle содержит неиспользуемые standalone компоненты…

Да, иногда tree-shaking не встряхивает такие компоненты, и размер приложения может постепенно увеличиваться. А если ты еще и создатель какой-то публичной библиотеки, то эта тема крайне актуальна для тебя. 

Разберем на конкретных примерах:

Standalone component встряхивается

Условия: 

  •  Есть standalone компонент (SC1), он ничего не импортирует

  •  AppModule импортирует SC1

    Компонент может встряхиваться
    Компонент может встряхиваться

standalone1.component.ts

import { Component } from '@angular/core';
import {CommonModule} from "@angular/common";

@Component({
 selector: 'app-standalone-1',
 templateUrl: './standalone-1.component.html',
 standalone: true, // указываем, что это standalone component
 imports: [],  // ничего не импортируем
})
export class Standalone1Component {
}

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { Standalone1Component } from "./standalone1/standalone1.component";
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
Standalone1Component,  // импортируем, но затем нигде не используем
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}

Так как standalone компонент ничего не импортирует и нигде не используется, то при tree-shaking он будет удален.

Аналогично и здесь: 

  •  standalone компонент (SC1) импортирует другой standalone компонент (SC2)

  •  AppModule импортирует SC1

    Компоненты так же могут встряхиваться
    Компоненты так же могут встряхиваться

standalone2.component.ts

import { Component } from '@angular/core';
import {CommonModule} from "@angular/common";
@Component({
selector: 'app-standalone-2',
templateUrl: './standalone-2.component.html',
standalone: true, // указываем, что это standalone component
imports: [],  // ничего не импортируем
})
export class Standalone2Component {
}

standalone1.component.ts

import { Component } from @angularr/core';
import {CommonModule} from @angularr/common";
@Component({
selector: 'app-standalone-1',
templateUrl: './standalone-1.component.html',
standalone: true, // указываем, что это standalone component
imports: [Standalone2Component],  // импортируем другой компонент
})
export class Standalone1Component {
}

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { Standalone1Component } from "./standalone1/standalone1.component";

@NgModule({
 declarations: [
   AppComponent
 ],
 imports: [
   BrowserModule,
   Standalone1Component,  // импортируем, но затем нигде не используем
 ],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule {
}

Так как ни один из standalone компонентов не импортирует модули, а Standalone1Component нигде не используется (кроме импорта в AppModule), то при tree-shaking такие компоненты (Standalone1Component, Standalone2Component) будут удалены.

Standalone component не встряхивается

Условия: 

  • standalone компонент (SC1) импортирует другие модули

  • AppModule импортирует SC1, но нигде не использует его

Компонент не будет встряхиваться
Компонент не будет встряхиваться

standalone1.component.ts

import { Component } from '@angular/core';
import {CommonModule} from "@angular/common";

@Component({
 selector: 'app-standalone-1',
 templateUrl: './standalone-1.component.html',
 standalone: true, // указываем, что это standalone component
 imports: [CommonModule],  // импортируем любой модуль
})
export class Standalone1Component {
}

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { Standalone1Component } from "./standalone1/standalone1.component";

@NgModule({
 declarations: [
   AppComponent
 ],
 imports: [
   BrowserModule,
   Standalone1Component,  // импортируем, но затем нигде не используем
 ],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule {
}

Так как компонент нигде не используется, кроме импорта в модуль, то не хотелось бы увидеть его в бандле приложения, но...

Запускаем ng build и в директории dist в main.js файле — видим код нашего standalone компонента:

return new(r||e)},e.\u0275cmp=fs({type:e,selectors:[["app-standalone-1"]],standalone:!0

Почему так получилось?

Фреймворку достаточно сложно определить, имеет ли импортируемый модуль внутри себя массив providers. Вероятно, провайдеры есть и они могут использоваться в приложении. Поэтому встряхивать компоненты, которые импортируют модули, может быть опасно.
И как было упомянуто в самом начале про tree-shakable providers: в случае указания провайдеров именно в модуле, мы лишаем себя возможности встряхивания.

Кстати, аналогичная ситуация (standalone component не встряхивается) будет и здесь: если компонент импортирует не модуль, а другой standalone component, который уже импортирует модуль.

Компоненты не будут встряхиваться
Компоненты не будут встряхиваться

Что можем сделать

Не хочется же иметь неиспользуемые зависимости у себя в приложении?

Тогда нужно:

  1. Внимательно относиться к imports в standalone компонентах.
    Импортируем то, что действительно требуется компоненту. Например, CommonModule — необязательно импортировать весь модуль, есть NgIf, NgForOf, AsyncPipe и другие standalone компоненты, использование которых улучшает процесс встряхивания.

  2. Экспортируемые модули создавать небольшими или вовсе экспортировать только коллекцию компонентов.

  3. Хорошая практика — использовать provideIn вместо providers модуля.

Заключение

Иногда долгожданные и удобные новинки в фреймворках таят в себе сюрпризы. Но это не повод отказываться от них, а лишь напоминание о том, что полезно иногда копнуть глубже. Вооружившись этой информацией, мы получили новый пункт в задаче оптимизации размера бандла: проверить зависимости в standalone компонентах.

Комментарии (0)