Angular Performance Optimization
Ahead-of-Time (AOT) Compilation
AOT compilation occurs during the build phase, unlike Just-in-Time (JIT) compilation, which happens at runtime. AOT offers several advantages:
- Reduced Startup Time: The browser doesn’t need to parse and compile templates at runtime.
- Smaller JavaScript Bundle Size: Compiled code is more compact.
- Improved Runtime Performance: Compiled code executes directly without additional compilation.
Enabling AOT Compilation
In the Angular CLI, enable AOT by modifying the angular.json file’s build configuration:
"architect": {
"build": {
"options": {
"aot": true
}
}
}Run ng build --prod or ng build --configuration=production to build your application with AOT compilation.
Lazy Loading Modules
Lazy loading is an optimization technique that loads modules and components on demand during runtime, rather than loading the entire application at startup. This significantly reduces initial load time and memory usage.
Implementing Lazy Loading
- Create Feature Modules: Generate a separate module for each major feature.
ng generate module feature --route feature- Configure Routes: In
app-routing.module.ts, use theloadChildrendirective to lazily load modules.
const routes: Routes = [
{ path: 'feature', loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule) },
// Other routes...
];- Update HTML: In
app.component.html, use the<router-outlet>tag to render lazily loaded modules.
<router-outlet></router-outlet>Change Detection Strategies
Change detection is Angular’s mechanism for detecting and updating the view. By default, Angular uses a “dirty-checking” mechanism, which can cause performance issues in complex applications.
- Default Change Detection Angular’s default strategy is
ChangeDetectionStrategy.Default, where the framework checks the entire component tree on every event, timer, or asynchronous operation. - OnPush Strategy Setting
changeDetectiontoChangeDetectionStrategy.OnPushinstructs Angular to check a component only when its input properties change or it receives an event, reducing unnecessary view updates.
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-my-component',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
// ...
}Optimizing Change Detection
- Avoid Unnecessary Input Changes: Only update input properties when necessary.
- Use Pure Pipes: Mark pipes as
pureto prevent unnecessary recalculations. - Leverage ngDoCheck: Use the
ngDoChecklifecycle hook for custom change detection logic when component state depends on non-input properties.
Combining AOT, Lazy Loading, and Change Detection
Using AOT compilation, lazy loading, and optimized change detection strategies together can significantly enhance Angular application performance. AOT reduces runtime compilation overhead, lazy loading minimizes initial load time, and optimized change detection reduces unnecessary view updates.
Code Splitting and Lazy Loading Components
Code splitting divides application code into smaller bundles loaded on demand, improving initial load time and user experience.
Implementing Code Splitting and Lazy Loading
- Create Feature Modules: Generate a module for each major feature to enable code splitting.
ng generate module feature --route feature- Configure Routes: In
app-routing.module.ts, useloadChildrenfor dynamic module loading.
const routes: Routes = [
{ path: 'feature', loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule) },
// Other routes...
];- Update HTML: Use
<router-outlet>inapp.component.htmlto render lazily loaded modules.
<router-outlet></router-outlet>- Optimize Lazy Loading: Ensure lazy-loaded modules include only necessary components and services. Avoid importing globally shared modules like
CommonModule, which can lead to duplicate code loading.
Image Optimization
Images are often the largest resources on a webpage, so optimizing them can significantly improve load times.
Image Optimization Strategies
- Use Appropriate Formats: Choose formats like JPEG, PNG, SVG, or WebP to balance file size and quality.
- Compress Images: Use tools like TinyPNG, ImageOptim, or online services to reduce image file sizes.
- Size Appropriately: Match image dimensions to their display size, avoiding CSS scaling to prevent unnecessary downloads.
- Lazy Load Images: Delay loading images outside the viewport until users scroll to them.
<img src="placeholder.jpg" data-src="actual-image.jpg" class="lazy-load">Implement lazy loading in TypeScript using a directive or libraries like ngx-lazy-load:
import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[appLazyLoad]'
})
export class LazyLoadDirective {
constructor(private el: ElementRef) {}
@HostListener('window:scroll', ['$event'])
onScroll(event) {
const rect = this.el.nativeElement.getBoundingClientRect();
if (rect.top <= window.innerHeight && rect.bottom >= 0) {
const img = this.el.nativeElement;
img.src = img.getAttribute('data-src');
img.removeAttribute('data-src');
}
}
}Use the directive in the template:
<img src="placeholder.jpg" [appLazyLoad]="true" data-src="actual-image.jpg">Angular Testing
Unit Testing with Jest
Installing and Configuring Jest
Jest, developed by Facebook, is a JavaScript testing framework known for its fast execution and rich feature set. To use Jest, install it and configure it for Angular.
Install Jest
npm install --save-dev jest @testing-library/jest-dom @testing-library/angular @types/jest ts-jestConfigure Jest
Edit or create jest.config.js in the project root to configure Jest’s environment and transformers:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
};Create setupTests.js to initialize the Angular testing environment:
import '@testing-library/jest-dom';
import { configureTestingModule } from './testing.module'; // Custom testing module configuration
beforeEach(() => {
configureTestingModule();
});Writing Component Tests
Testing Angular components with Jest involves mocking services, rendering components, and interacting with them.
Component Test Example
Consider a MyComponent that depends on a DataService:
// my.component.ts
import { Component } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'app-my-component',
template: `
<p>{{ data }}</p>
<button (click)="fetchData()">Fetch Data</button>
`,
})
export class MyComponent {
data: string;
constructor(private dataService: DataService) {}
fetchData() {
this.dataService.getData().subscribe(res => {
this.data = res;
});
}
}Writing Tests
In my.component.spec.ts, write the tests:
import { render, screen, fireEvent } from '@testing-library/angular';
import { MyComponent } from './my.component';
import { of } from 'rxjs';
import { DataService } from '../data.service';
describe('MyComponent', () => {
const mockDataService = {
getData: () => of('Test Data'),
};
beforeEach(async () => {
await render(MyComponent, {
providers: [{ provide: DataService, useValue: mockDataService }],
});
});
it('displays data after fetching', async () => {
const button = screen.getByRole('button');
fireEvent.click(button);
expect(await screen.findByText(/Test Data/i)).toBeInTheDocument();
});
});Unit Testing with Karma
Installing and Configuring Karma
Karma is the default testing framework supported by Angular CLI, ideal for unit and integration testing in Angular projects.
Install Karma
If using an Angular CLI-generated project, Karma is pre-installed.
Configure Karma
Configure Karma in karma.conf.js:
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
files: [
{ pattern: './src/test.ts', watched: false },
],
preprocessors: {
'./src/test.ts': ['webpack', 'sourcemap'],
},
webpack: {
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: 'ts-loader',
options: {
compilerOptions: require('./tsconfig.spec.json').compilerOptions,
},
},
],
exclude: /node_modules/,
},
],
},
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true,
});
};Writing Service Tests
Service unit tests typically involve mocking external dependencies and verifying method behavior.
Service Test Example
Consider a DataService that fetches data from an API:
// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) {}
getData() {
return this.http.get('https://api.example.com/data');
}
}Writing Tests
In data.service.spec.ts, write the tests:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let httpMock: HttpTestingController;
let service: DataService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService],
});
httpMock = TestBed.inject(HttpTestingController);
service = TestBed.inject(DataService);
});
it('should fetch data from the API', () => {
const testData = { message: 'Hello, World!' };
service.getData().subscribe(data => {
expect(data).toEqual(testData);
});
const req = httpMock.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('GET');
req.flush(testData);
});
});End-to-End Testing with Protractor
Protractor is an end-to-end testing framework for Angular and AngularJS applications, built on WebDriverJS. It provides APIs to simulate user interactions like clicking buttons, filling forms, and asserting page elements.
Installing Protractor
Ensure Node.js and npm are installed, then install Protractor and dependencies:
npm install -g protractor
npm install --save-dev protractor @types/protractorConfiguring Protractor
Protractor requires a configuration file. Create protractor.conf.js in the project root:
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
}
};Writing End-to-End Tests
End-to-end tests focus on complete application workflows from a user’s perspective. Below is an example testing a login flow.
Test File Structure
Create test files in the e2e directory, e.g., login.e2e-spec.ts.
Test Example
import { browser, by, element } from 'protractor';
describe('Login E2E Test', function() {
beforeEach(() => {
browser.get('/login');
});
it('should login with valid credentials', () => {
element(by.model('username')).sendKeys('testuser');
element(by.model('password')).sendKeys('testpass');
element(by.id('login-button')).click();
browser.wait(() => element(by.css('.welcome-message')).isPresent(), 10000);
expect(element(by.css('.welcome-message')).getText()).toEqual('Welcome, testuser!');
});
it('should not login with invalid credentials', () => {
element(by.model('username')).sendKeys('invaliduser');
element(by.model('password')).sendKeys('invalidpass');
element(by.id('login-button')).click();
browser.wait(() => element(by.css('.error-message')).isPresent(), 10000);
expect(element(by.css('.error-message')).getText()).toEqual('Invalid username or password.');
});
});Running End-to-End Tests
Ensure the Angular application is running, then execute:
protractor ./protractor.conf.jsAdvanced Techniques
- Page Object Pattern: Organize and reuse code by creating a class for each page, encapsulating elements and actions.
// login.po.ts
export class LoginPage {
navigateTo() {
return browser.get('/');
}
setUsername(username) {
return element(by.model('username')).sendKeys(username);
}
setPassword(password) {
return element(by.model('password')).sendKeys(password);
}
clickLoginButton() {
return element(by.id('login-button')).click();
}
getErrorMessage() {
return element(by.css('.error-message')).getText();
}
}Use the page object in tests:
import { LoginPage } from '../pages/login.po';
describe('Login E2E Test', function() {
let loginPage: LoginPage;
beforeEach(() => {
loginPage = new LoginPage();
loginPage.navigateTo();
});
it('should not login with invalid credentials', () => {
loginPage.setUsername('invaliduser');
loginPage.setPassword('invalidpass');
loginPage.clickLoginButton();
browser.wait(() => loginPage.getErrorMessage().then(text => text !== ''), 10000);
expect(loginPage.getErrorMessage()).toEqual('Invalid username or password.');
});
});- Test Data Management: Use external files (e.g., JSON) to manage test data for flexibility and maintainability.
- Parallel Testing: Leverage Protractor’s multi-browser and parallel testing capabilities to run tests simultaneously, speeding up the process.
End-to-End Testing with Cypress
Cypress is a modern end-to-end testing framework offering rich features and an intuitive API for testing web applications. Its real-time test rerun, time-travel debugging, and network request interception make it ideal for Angular end-to-end testing.
Installing Cypress
Ensure Node.js and npm are installed, then install Cypress in the project root:
npm install cypress --save-devInitializing Cypress
Run Cypress to initialize it and create necessary configuration files:
npx cypress openThis launches Cypress’s GUI, where you can run tests, view results, and configure settings.
Configuring Cypress
Cypress uses a cypress.json file for configuration. Create or edit it in the project root:
{
"baseUrl": "http://localhost:4200",
"defaultCommandTimeout": 10000,
"pageLoadTimeout": 20000,
"video": true,
"videoUploadOnPasses": false
}Writing End-to-End Tests
Cypress uses Mocha as its test runner and Chai for assertions. Tests are typically stored in the cypress/integration directory.
Example: Login Test
Create a test file, e.g., login_spec.js:
describe('Login page', () => {
beforeEach(() => {
cy.visit('/login');
});
it('logs in with correct credentials', () => {
cy.get('#username').type('testuser');
cy.get('#password').type('testpass{enter}');
cy.url().should('include', '/dashboard');
cy.contains('Welcome, testuser!').should('be.visible');
});
it('displays error for incorrect credentials', () => {
cy.get('#username').type('wronguser');
cy.get('#password').type('wrongpass{enter}');
cy.contains('Invalid username or password').should('be.visible');
});
});Running Tests
Run tests via Cypress’s GUI or the command line:
npx cypress runAdvanced Features
- Time-Travel Debugging: Cypress’s debugging tool allows tracing back every test action, aiding in issue identification.
- Network Request Interception: Use
cy.intercept()to mock server responses, useful for testing frontend apps reliant on backend APIs. - Test Isolation: Use
beforeEach()andafterEach()hooks to set up and clean test environments, ensuring test independence.
Integration with CI/CD
Integrate Cypress tests into CI/CD pipelines. For example, add a job in .gitlab-ci.yml to run Cypress tests:
stages:
- test
cypress:
stage: test
script:
- npx cypress run



