#### Change Detection - signals offer finer change detection granularity - concerns maintaining [[core web vitals]] - choice to go `zoneless` ##### Zone.js - triggers angular change detection on async browser events - monkey-patches browser API like `addEventListener`, `setTimeout`, and `Promise.then` to control async operations ##### Zoneless Mode - new in Angular 19 - set by removing zone.js from polyfill in angular.json and adding `provideExperimentalZonelessChangeDetection()` to global providers - event handling - `runInContext`: tells Angular to run change detection after an event or callback: - zoneless mode necessary to run any non-template based async code in runInContext ```ts setTimeout(() => { runInContext(() => // async operation); }, 1000); ``` - event binding syntax: `(click)="handler()"` ##### Component Change Detection Strategy - set globally with `{ provide: DEFAULT_CHANGE_DETECTION_STRATEGY, useValue: ChangeDetectionStrategy.OnPush }` - `OnPush` re-renders when: - component input reassigned(reference) - Events emitted - component DOM event - this or child component output event - `ChangeDetectorRef` manual trigger (markForCheck, detectChanges) - async piped observable emits within template - template signal mutation (Angular 17) ##### Methodologies ###### FormGroup/FormArray Listen on ValueChanges ###### Async Pipe In Template 1. Subscribes to an Observable or Promise 2. Returns the latest emitted value 3. Automatically unsubscribes when the component is destroyed 4. marks component for check when new values arrive ###### Component Managed Observables - manually subscribe and unsubscribe - manually trigger change detection with `OnPush` CD `markForCheck` - set component variables that the template picks up on ###### Signals - track mutations and reassignments - `compute` method: creates a read only signal that reacts updates from dependent signals ```ts // Immutable Programming Style userSignal.set({...userSignal(), age: userSignal().age + 1}); // Mutable style userSignal.update(user => { user.age += 1; return user; }); // Create a signal with an object const user = signal({ name: 'John', age: 30 }); // Approach 1: Mutate outside, then set (works but less idiomatic) const userObj = user(); // Get current value userObj.name = 'Jane'; // Mutate it userObj.age = 31; user.set(userObj); // Set it back // Approach 2: More idiomatic with update (preferred) user.update(currentUser => { currentUser.name = 'Jane'; currentUser.age = 31; return currentUser; }); ``` ##### Old Way - angular change detection checks components and updates DOM - unit of granularity = component: entire component re-renders if any data it used changed - problem: change detection run on any async event not when necessary ###### Default Change detection ```ts // PARENT COMPONENT @Component({ selector: 'app-parent', template: ` <h2>Parent: {{ user.name }}</h2> <button (click)="updateName()">Update Name</button> <app-child [user]="user"></app-child> `, // Default change detection }) export class ParentComponent { user = { name: 'John' }; updateName() { // Mutating the object directly works with Default change detection this.user.name = 'Jane'; } } // CHILD COMPONENT @Component({ selector: 'app-child', template: `<p>Child sees: {{ user.name }}</p>`, // Default change detection }) export class ChildComponent { @Input() user: { name: string }; } ``` ###### OnPush Change Detection - must follow immutability practices ```ts // PARENT COMPONENT @Component({ selector: 'app-parent', template: ` <h2>Parent: {{ user.name }}</h2> <button (click)="updateNameImmutable()">Update Immutable</button> <button (click)="updateNameMutable()">Update Mutable</button> <app-child [user]="user"></app-child> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ParentComponent { user = { name: 'John' }; updateNameImmutable() { // Creates new reference - works with OnPush this.user = { name: 'Jane' }; } updateNameMutable() { // Mutation doesn't work with OnPush without help this.user.name = 'Jane'; // Parent won't update without a trigger like an event } } // CHILD COMPONENT @Component({ selector: 'app-child', template: `<p>Child sees: {{ user.name }}</p>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class ChildComponent implements OnChanges { @Input() user: { name: string }; ngOnChanges(changes: SimpleChanges) { console.log('Input changed:', changes); // Only triggered when user reference changes, not on property mutation } } ``` ##### New Way - unit of granularity = DOM node: when a signal changes the individual DOM nodes it's associated within the template update. This is more granular than even an HTML tag, think: - text nodes - element attributes/properties ##### Global Signal State Store Pattern - similar to state management solutions like NgRx, but with less boilerplate - #### Modules Vs. Stand Alone Components - The Component becomes the primary unit of application organization - simplifies app architecture by removing the parallel hierarchy (organizational complexity) created by NgModules #### Global Dependency and No Lazy Loading Vs. Local Component Imports With Lazy Loading | Metric | Global Dependencies, No Lazy Loading | Local Component Imports with Lazy Loading | Advantage Goes To | | -------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------- | | Initial Bundle Size | Larger (everything at once) | Smaller (only what's needed for first view) | Local/Lazy | | Subsequent Navigation Speed | Faster (already loaded) | Slightly slower (may need to load new chunks) | Global | | Cold Start Time | Longer | Shorter | Local/Lazy | | Hot Start Time (cached) | Similar | Similar | Neutral | | Time-to-Interactive (Initial) | Longer | Shorter | Local/Lazy | | Time-to-Interactive (Subsequent) | Immediate | Brief delay possible | Global | | Runtime Memory Usage | More efficient (shared instances) | Slightly higher (potential duplication) | Global | | Compile Time | Potentially faster | Slightly longer | Global | | Build Complexity | Simpler | More complex | Global | | Developer Mental Overhead | Very low | Moderate | Global | | Code Maintenance | Simpler (centralized dependencies) | More complex (distributed dependencies) | Global | | Application Scalability | Centralized dependency management, clearer collaboration points | distributed dependency management, smaller chunks, more integration points | Global | For your specific context (desktop application, tolerant users, infrequent releases), the global approach seems to offer more advantages, particularly in developer experience and simplicity. The main trade-off is the initially larger bundle size, but if that's acceptable to your users, the global approach could be the better choice. - global dependencies negate the benefit of lazy loading as there is 1 bundle for all the code ##### `main.ts` ###### Global Dependency No Lazy Loading ```ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { appConfig } from './app/app.config'; // Bootstrap the application with the configuration from app.config.ts bootstrapApplication(AppComponent, appConfig) .catch(err => console.error('Error bootstrapping app:', err)); ``` ###### Local Component Imports With Lazy Loading (SAME) ##### `app.config.ts` ###### Global Dependency No Lazy Loading ```ts import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { importProvidersFrom } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; // Shared components import { HeaderComponent } from './shared/header.component'; import { FooterComponent } from './shared/footer.component'; // Services import { UserService } from './services/user.service'; import { DataService } from './services/data.service'; export const appConfig: ApplicationConfig = { providers: [ // Core Angular features - use modern provider functions provideHttpClient(), provideAnimations(), // Common modules importProvidersFrom( CommonModule ), // Services UserService, DataService, // Components importProvidersFrom( HeaderComponent, FooterComponent ) // Routing provideRouter(routes), ] }; ``` ###### Local Component Imports With Lazy Loading ```ts import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; export const appConfig: ApplicationConfig = { providers: [ // Core Angular features - use modern provider functions provideHttpClient(), provideAnimations(), // Routing with lazy loading defined in app.routes.ts provideRouter(routes) ] }; ``` - could globally load just obvious singleton services ##### `app.route.ts` ###### Global Dependency No Lazy Loading ```ts import { Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; import { ProfileComponent } from './profile/profile.component'; import { SettingsComponent } from './settings/settings.component'; // no lazy loading export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'profile', component: ProfileComponent }, { path: 'settings', component: SettingsComponent } ]; ``` ###### Local Component Imports With Lazy Loading ```ts import { Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; // lazy loading export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'profile', loadComponent: () => import('./profile/profile.component').then(m => m.ProfileComponent) }, { path: 'settings', loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent) } ]; ``` ##### `profile.component.ts` ###### Global Dependency No Lazy Loading ```ts import { Component } from '@angular/core'; import { UserService } from '../services/user.service'; @Component({ selector: 'app-profile', standalone: true, // not actually needed, defaulted in Angular 19 template: ` <h1>Profile</h1> <p>Welcome, {{this.userService.getCurrentUser().name}}</p> ` }) export class ProfileComponent { // injected from global providers constructor(private userService: UserService) { } } ``` ###### Local Component Imports With Lazy Loading ```ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HeaderComponent } from '../shared/header.component'; import { UserService } from '../services/user.service'; @Component({ selector: 'app-profile', standalone: true, // not actually needed, defaulted in Angular 19 // clearly identify all dependency of the component imports: [ CommonModule, HeaderComponent ], providers: [ UserService // Local instance for this component ], template: ` <app-header></app-header> <h1>Profile</h1> <p>Welcome, {{this.userService.getCurrentUser().name}}</p> ` }) export class ProfileComponent { constructor(private userService: UserService) { } } ``` ##### Current App Bundle Sizes ![[Pasted image 20250327112723.png]] 1,567,419 characters in main-es2015 are app code out of 9,975,392 ~ 1.5MB so in total less than 3MB of source code ##### Conclusion - we should prioritize simplifying the app by using global dependencies and no lazy loading method - our app code currently bundles (unminified) at < 3MB and will be cached expect for our bi-weekly deploys - runtime experience both time to interactive and memory utilization will be slightly better with this approach - the benefits of declaring all imports in standalone components is: - explicit import utilization - slightly better bundle compression (better tree-shaking) - but is much more verbose and adds mental load to development - full global dependencies creates a clarity and simplicity of application structure. - we can add existing modules to imports and transition the angular app to standalone component only slowly if we desire