diff --git a/README.md b/README.md index 8a971e4..1d7c198 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,11 @@ If you would like to propose a challenge, this project is open source, so feel f bug CD decoupling +
+Angular performance + +default onPush +
Typescript @@ -96,7 +101,7 @@ If you would like to propose a challenge, this project is open source, so feel f - +
Thomas Laforge
Thomas Laforge

33 🧩
Thomas Laforge
Thomas Laforge

34 🧩
diff --git a/apps/performance/default-onpush/.eslintrc.json b/apps/performance/default-onpush/.eslintrc.json new file mode 100644 index 0000000..bf8df14 --- /dev/null +++ b/apps/performance/default-onpush/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "app", + "style": "kebab-case" + } + ] + }, + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/apps/performance/default-onpush/README.md b/apps/performance/default-onpush/README.md new file mode 100644 index 0000000..07a6e71 --- /dev/null +++ b/apps/performance/default-onpush/README.md @@ -0,0 +1,58 @@ +

OnPush to optimize Change Detection

+ +> Author: Thomas Laforge + +### Information + +In this series of challenges, you will learn how to optimize and enhance the performance of your Angular Application. + +The first step is to download the [Angular DevTools Chrome extention](https://chrome.google.com/webstore/detail/angular-devtools/ienfalfjdbdpebioblfackkekamfmbnh) if you haven't already done so. This extension allows you to profile your application and detect performance issues. + +In this challenge, we will explore the differences and impacts of using `ChangeDetectionStrategy.Default` versus `ChangeDetectionStrategy.OnPush`. To provide a clearer demonstration, I have added counters to each component and each row in our application. However, in real-world scenarios, you may not have such counters. This is where the Angular DevTool profiler comes to the rescue. + +Start by serving this application by running: `npx nx serve performance-default-onpush` inside your terminal. Then open Chrome DevTool by pressing **F12** and switch to the Angular Tab. From there you can select the Profiler tab as shown below. + +![profiler tab](./img/profiler-tab.png 'Profiler tab') + +Start profiling your application and type some letters inside the input field. You will notice that each number is increasing and the profiler will show you a lot of change detection cycle. + +If you click on one of the bars (indicated by the yellow arrow on the picture below), you can see that `PersonListComponent`, `RandomComponent` and all the `MatListItem` are impacted by the change detection cycle, even when we only interact with the input field. + +![profiler record](./img/profiler-record.png 'Profiler Record') + +### Statement + +The goal of this challenge is to improve the clustering of change detection within the application. + +### Hints: + +
+ Hint 1 + +Use `ChangeDetectionStrategy.OnPush` but this will not be enough. + +
+ +
+ Hint 2 + +Create smaller components to better separate the input field from the list. + +
+ +### Submitting your work + +1. Fork the project +2. clone it +3. npm ci +4. `npx nx serve performance-default-onpush` +5. _...work on it_ +6. Commit your work +7. Submit a PR with a title beginning with **Answer:34** that I will review and other dev can review. + +performance-default-onpush +performance-default-onpush solution author + + + +_You can ask any question on_ twitter diff --git a/apps/performance/default-onpush/img/profiler-record.png b/apps/performance/default-onpush/img/profiler-record.png new file mode 100644 index 0000000..febcbd2 Binary files /dev/null and b/apps/performance/default-onpush/img/profiler-record.png differ diff --git a/apps/performance/default-onpush/img/profiler-tab.png b/apps/performance/default-onpush/img/profiler-tab.png new file mode 100644 index 0000000..a6d228d Binary files /dev/null and b/apps/performance/default-onpush/img/profiler-tab.png differ diff --git a/apps/performance/default-onpush/project.json b/apps/performance/default-onpush/project.json new file mode 100644 index 0000000..571c029 --- /dev/null +++ b/apps/performance/default-onpush/project.json @@ -0,0 +1,84 @@ +{ + "name": "performance-default-onpush", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "app", + "sourceRoot": "apps/performance/default-onpush/src", + "tags": [], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/performance/default-onpush", + "index": "apps/performance/default-onpush/src/index.html", + "main": "apps/performance/default-onpush/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/performance/default-onpush/tsconfig.app.json", + "assets": [ + "apps/performance/default-onpush/src/favicon.ico", + "apps/performance/default-onpush/src/assets" + ], + "styles": [ + "apps/performance/default-onpush/src/styles.scss", + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "performance-default-onpush:build:production" + }, + "development": { + "browserTarget": "performance-default-onpush:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "performance-default-onpush:build" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "apps/performance/default-onpush/**/*.ts", + "apps/performance/default-onpush/**/*.html" + ] + } + } + } +} diff --git a/apps/performance/default-onpush/src/app/app.component.ts b/apps/performance/default-onpush/src/app/app.component.ts new file mode 100644 index 0000000..ac19efc --- /dev/null +++ b/apps/performance/default-onpush/src/app/app.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { PersonListComponent } from './person-list.component'; +import { Persons } from './persons'; +import { RandomComponent } from './random.component'; + +@Component({ + standalone: true, + imports: [PersonListComponent, RandomComponent], + selector: 'app-root', + template: ` + + +
+ + +
+ `, +}) +export class AppComponent { + personList = [...Persons]; + person2List = [...Persons]; +} diff --git a/apps/performance/default-onpush/src/app/app.config.ts b/apps/performance/default-onpush/src/app/app.config.ts new file mode 100644 index 0000000..59198e6 --- /dev/null +++ b/apps/performance/default-onpush/src/app/app.config.ts @@ -0,0 +1,6 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideAnimations } from '@angular/platform-browser/animations'; + +export const appConfig: ApplicationConfig = { + providers: [provideAnimations()], +}; diff --git a/apps/performance/default-onpush/src/app/person-list.component.ts b/apps/performance/default-onpush/src/app/person-list.component.ts new file mode 100644 index 0000000..1e5d8a6 --- /dev/null +++ b/apps/performance/default-onpush/src/app/person-list.component.ts @@ -0,0 +1,72 @@ +import { Component, Input } from '@angular/core'; + +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { Person } from './person'; + +@Component({ + selector: 'app-person-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatListModule, + MatFormFieldModule, + MatInputModule, + MatChipsModule, + ], + template: ` +

+ {{ title }} +

+ + + + + + +
Empty list
+ +
+

+ {{ item.label }} +

+
+ {{ count(item) }} +
+
+
+ +
+ `, + host: { + class: 'w-full flex flex-col items-center', + }, +}) +export class PersonListComponent { + @Input() data: Person[] | null = null; + @Input() title = ''; + + label = ''; + + handleKey(event: any) { + if (event.keyCode === 13) { + this.data?.unshift({ label: this.label, num: 0 }); + this.label = ''; + } + } + + count(person: Person) { + person.num++; + return person.num; + } +} diff --git a/apps/performance/default-onpush/src/app/person.ts b/apps/performance/default-onpush/src/app/person.ts new file mode 100644 index 0000000..a656c2c --- /dev/null +++ b/apps/performance/default-onpush/src/app/person.ts @@ -0,0 +1,4 @@ +export interface Person { + label: string; + num: number; +} diff --git a/apps/performance/default-onpush/src/app/persons.ts b/apps/performance/default-onpush/src/app/persons.ts new file mode 100644 index 0000000..47293a0 --- /dev/null +++ b/apps/performance/default-onpush/src/app/persons.ts @@ -0,0 +1,284 @@ +import { Person } from './person'; + +export const Persons: Person[] = [ + { + label: 'Nettle', + num: 0, + }, + { + label: 'Rosalie', + num: 0, + }, + { + label: 'Rhona', + num: 0, + }, + { + label: 'Talyah', + num: 0, + }, + { + label: 'Fancie', + num: 0, + }, + { + label: 'Kari', + num: 0, + }, + { + label: 'Caresa', + num: 0, + }, + { + label: 'Gerta', + num: 0, + }, + { + label: 'Modestine', + num: 0, + }, + { + label: 'Emogene', + num: 0, + }, + { + label: 'Henryetta', + num: 0, + }, + { + label: 'Darelle', + num: 0, + }, + { + label: 'Clementine', + num: 0, + }, + { + label: 'Arabella', + num: 0, + }, + { + label: 'Constance', + num: 0, + }, + { + label: 'Josey', + num: 0, + }, + { + label: 'Talia', + num: 0, + }, + { + label: 'Loleta', + num: 0, + }, + { + label: 'Tedda', + num: 0, + }, + { + label: 'Francine', + num: 0, + }, + { + label: 'Kittie', + num: 0, + }, + { + label: 'Merry', + num: 0, + }, + { + label: 'Lexi', + num: 0, + }, + { + label: 'Dorie', + num: 0, + }, + { + label: 'Daron', + num: 0, + }, + { + label: 'Bella', + num: 0, + }, + { + label: 'Ashien', + num: 0, + }, + { + label: 'Hyacinthia', + num: 0, + }, + { + label: 'Beatrix', + num: 0, + }, + { + label: 'Tamiko', + num: 0, + }, + { + label: 'Gusty', + num: 0, + }, + { + label: 'Talyah', + num: 0, + }, + { + label: 'Cynthy', + num: 0, + }, + { + label: 'Trudy', + num: 0, + }, + { + label: 'Katharine', + num: 0, + }, + { + label: 'Kelci', + num: 0, + }, + { + label: 'Allyce', + num: 0, + }, + { + label: 'Dorthea', + num: 0, + }, + { + label: 'Patty', + num: 0, + }, + { + label: 'Fernandina', + num: 0, + }, + { + label: 'Fanya', + num: 0, + }, + { + label: 'Lauralee', + num: 0, + }, + { + label: 'Nanice', + num: 0, + }, + { + label: 'Maureen', + num: 0, + }, + { + label: 'Wilmette', + num: 0, + }, + { + label: 'Ellie', + num: 0, + }, + { + label: 'Maybelle', + num: 0, + }, + { + label: 'Angelina', + num: 0, + }, + { + label: 'Dawn', + num: 0, + }, + { + label: 'Arlene', + num: 0, + }, + { + label: 'Maryanne', + num: 0, + }, + { + label: 'Alyda', + num: 0, + }, + { + label: 'Alisun', + num: 0, + }, + { + label: 'Liana', + num: 0, + }, + { + label: 'Alejandrina', + num: 0, + }, + { + label: 'Costanza', + num: 0, + }, + { + label: 'Wendie', + num: 0, + }, + { + label: 'Rosalinda', + num: 0, + }, + { + label: 'Annabela', + num: 0, + }, + { + label: 'Kelsi', + num: 0, + }, + { + label: 'Morgana', + num: 0, + }, + { + label: 'Gabriel', + num: 0, + }, + { + label: 'Cherianne', + num: 0, + }, + { + label: 'Belia', + num: 0, + }, + { + label: 'Jillayne', + num: 0, + }, + { + label: 'Deerdre', + num: 0, + }, + { + label: 'Tedra', + num: 0, + }, + { + label: 'Reine', + num: 0, + }, + { + label: 'Anett', + num: 0, + }, + { + label: 'Donielle', + num: 0, + }, +]; diff --git a/apps/performance/default-onpush/src/app/random.component.ts b/apps/performance/default-onpush/src/app/random.component.ts new file mode 100644 index 0000000..3fc717c --- /dev/null +++ b/apps/performance/default-onpush/src/app/random.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +@Component({ + selector: 'app-random', + standalone: true, + template: `I do nothing but I'm here: {{ count() }}`, +}) +export class RandomComponent { + counter = 0; + + count() { + return this.counter++; + } +} diff --git a/apps/performance/default-onpush/src/assets/.gitkeep b/apps/performance/default-onpush/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/performance/default-onpush/src/favicon.ico b/apps/performance/default-onpush/src/favicon.ico new file mode 100644 index 0000000..317ebcb Binary files /dev/null and b/apps/performance/default-onpush/src/favicon.ico differ diff --git a/apps/performance/default-onpush/src/index.html b/apps/performance/default-onpush/src/index.html new file mode 100644 index 0000000..3941588 --- /dev/null +++ b/apps/performance/default-onpush/src/index.html @@ -0,0 +1,13 @@ + + + + + service-worker + + + + + + + + diff --git a/apps/performance/default-onpush/src/main.ts b/apps/performance/default-onpush/src/main.ts new file mode 100644 index 0000000..514c89a --- /dev/null +++ b/apps/performance/default-onpush/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); diff --git a/apps/performance/default-onpush/src/styles.scss b/apps/performance/default-onpush/src/styles.scss new file mode 100644 index 0000000..77e408a --- /dev/null +++ b/apps/performance/default-onpush/src/styles.scss @@ -0,0 +1,5 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/performance/default-onpush/tailwind.config.js b/apps/performance/default-onpush/tailwind.config.js new file mode 100644 index 0000000..38183db --- /dev/null +++ b/apps/performance/default-onpush/tailwind.config.js @@ -0,0 +1,14 @@ +const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); +const { join } = require('path'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), + ...createGlobPatternsForDependencies(__dirname), + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/performance/default-onpush/tsconfig.app.json b/apps/performance/default-onpush/tsconfig.app.json new file mode 100644 index 0000000..5822042 --- /dev/null +++ b/apps/performance/default-onpush/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/performance/default-onpush/tsconfig.editor.json b/apps/performance/default-onpush/tsconfig.editor.json new file mode 100644 index 0000000..4ee6393 --- /dev/null +++ b/apps/performance/default-onpush/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": [] + } +} diff --git a/apps/performance/default-onpush/tsconfig.json b/apps/performance/default-onpush/tsconfig.json new file mode 100644 index 0000000..51c7908 --- /dev/null +++ b/apps/performance/default-onpush/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.editor.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +}