From Bloated to Blazing: How Proper Angular Service Scoping Cut My App Size by 40%
Quick question: How many times have you added providedIn: 'root' to an Angular service without thinking twice?
If you're like most developers (myself included for way too long), the answer is probably "every single time." But here's the thing β that innocent little line might be quietly bloating your app and creating memory leaks you didn't even know existed.
By the end of this article, you'll know:
- Why root-level services aren't always the best choice
- When and how to scope services properly
- Practical examples with real code you can use today
- How to write unit tests for scoped services
- Pro tips that'll make your Angular apps leaner and faster
π¬ Before we dive in, tell me: Have you ever wondered why your Angular app feels sluggish even with lazy loading? Drop a comment β I bet service scoping is part of the puzzle!
The Root Problem (Pun Intended) π€
Let's start with a scenario that probably sounds familiar. You're building an e-commerce app, and you create a ProductService:
@Injectable({
providedIn: 'root' // π The automatic choice
})
export class ProductService {
private products: Product[] = [];
constructor(private http: HttpClient) {
// Load all products immediately
this.loadProducts();
}
private loadProducts() {
this.http.get<Product[]>('/api/products').subscribe(
products => this.products = products
);
}
getProducts(): Product[] {
return this.products;
}
}
Looks innocent, right? But here's what's happening under the hood:
- Your service loads ALL products the moment your app starts
- It stays in memory even when users are on completely unrelated pages
- It's creating unnecessary HTTP requests before users even need the data
Question for you: How many of your services are doing exactly this? π€―
The Smarter Approach: Strategic Service Scoping π―
Instead of making everything root-level, let's be intentional about where our services live. Here are the three main scoping strategies:
1. Component-Level Scoping
Perfect for services that should have a fresh instance per component:
@Injectable()
export class ShoppingCartService {
private items: CartItem[] = [];
addItem(item: CartItem) {
this.items.push(item);
}
getItems(): CartItem[] {
return [...this.items];
}
clearCart() {
this.items = [];
}
}
// In your component
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
providers: [ShoppingCartService] // π Scoped to this component
})
export class ProductListComponent {
constructor(private cartService: ShoppingCartService) {}
}
2. Module-Level Scoping
Great for services that should be shared within a feature module:
@Injectable()
export class UserProfileService {
private userProfile: UserProfile | null = null;
constructor(private http: HttpClient) {}
loadUserProfile(userId: string): Observable<UserProfile> {
if (!this.userProfile) {
return this.http.get<UserProfile>(`/api/users/${userId}`).pipe(
tap(profile => this.userProfile = profile)
);
}
return of(this.userProfile);
}
}
// In your feature module
@NgModule({
providers: [UserProfileService], // π Scoped to this module
// ... other module config
})
export class UserModule {}
3. Smart Root-Level Services
Only use root-level for truly global services:
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
private currentUser$ = new BehaviorSubject<User | null>(null);
constructor(private http: HttpClient) {
// Only load auth state - truly global concern
this.loadStoredAuthState();
}
// Methods that are needed app-wide
getCurrentUser(): Observable<User | null> {
return this.currentUser$.asObservable();
}
}
π‘ Pro tip: Ask yourself: "Does every single page of my app need this service?" If not, don't make it root-level!
Real-World Example: E-Commerce Product Categories ποΈ
Let's build a practical example. Imagine you have different product categories, and each category has its own complex filtering logic:
// β Bad: Root-level service loading everything
@Injectable({
providedIn: 'root'
})
export class BadProductService {
private electronicsProducts: Product[] = [];
private clothingProducts: Product[] = [];
private booksProducts: Product[] = [];
constructor(private http: HttpClient) {
// Loading everything immediately - wasteful!
this.loadAllCategories();
}
private loadAllCategories() {
// This runs even if user only visits electronics
this.http.get<Product[]>('/api/electronics').subscribe(/*...*/);
this.http.get<Product[]>('/api/clothing').subscribe(/*...*/);
this.http.get<Product[]>('/api/books').subscribe(/*...*/);
}
}
// β
Good: Category-specific services
@Injectable()
export class ElectronicsService {
private products: Product[] = [];
private filters: ElectronicsFilter = {};
constructor(private http: HttpClient) {}
loadProducts(): Observable<Product[]> {
if (this.products.length === 0) {
return this.http.get<Product[]>('/api/electronics').pipe(
tap(products => this.products = products)
);
}
return of(this.products);
}
applyFilters(filters: ElectronicsFilter): Product[] {
// Category-specific filtering logic
return this.products.filter(product => {
return this.matchesElectronicsFilters(product, filters);
});
}
private matchesElectronicsFilters(product: Product, filters: ElectronicsFilter): boolean {
// Complex electronics-specific filtering
return true; // Simplified for demo
}
}
// Provide it only in the Electronics module
@NgModule({
providers: [ElectronicsService],
// ... other config
})
export class ElectronicsModule {}
Think about it: Why should your app load clothing filters when the user is browsing electronics? π€·ββοΈ
Unit Testing Scoped Services π§ͺ
Here's how to properly test your scoped services:
describe('ElectronicsService', () => {
let service: ElectronicsService;
let httpMock: jasmine.SpyObj<HttpClient>;
beforeEach(() => {
const httpSpy = jasmine.createSpyObj('HttpClient', ['get']);
TestBed.configureTestingModule({
providers: [
ElectronicsService,
{ provide: HttpClient, useValue: httpSpy }
]
});
service = TestBed.inject(ElectronicsService);
httpMock = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
});
it('should load products only once', fakeAsync(() => {
const mockProducts = [
{ id: 1, name: 'iPhone', category: 'electronics' },
{ id: 2, name: 'MacBook', category: 'electronics' }
];
httpMock.get.and.returnValue(of(mockProducts));
// First call
service.loadProducts().subscribe();
tick();
// Second call
service.loadProducts().subscribe();
tick();
// HTTP should be called only once
expect(httpMock.get).toHaveBeenCalledTimes(1);
}));
it('should apply filters correctly', () => {
// Test your filtering logic
const filters: ElectronicsFilter = { brand: 'Apple', maxPrice: 1000 };
const results = service.applyFilters(filters);
expect(results).toBeDefined();
// Add more specific assertions based on your filter logic
});
});
Quick question: How do you usually test your services? Are you testing the scoping behavior too? π
Pro Tips That'll Make You Stand Out π₯
1. Lazy Service Loading Pattern
@Injectable()
export class LazyDataService {
private data$ = new BehaviorSubject<any[]>([]);
constructor(private http: HttpClient) {}
getData(): Observable<any[]> {
if (this.data$.getValue().length === 0) {
this.http.get<any[]>('/api/data').subscribe(
data => this.data$.next(data)
);
}
return this.data$.asObservable();
}
}
2. Service Cleanup Pattern
@Injectable()
export class CleanupService implements OnDestroy {
private destroy$ = new Subject<void>();
constructor(private http: HttpClient) {}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
getData(): Observable<any> {
return this.http.get('/api/data').pipe(
takeUntil(this.destroy$)
);
}
}
3. Service Factory Pattern
export function createCategoryService(category: string) {
return new CategoryService(category);
}
@NgModule({
providers: [
{
provide: CategoryService,
useFactory: createCategoryService,
deps: ['categoryName']
}
]
})
export class DynamicCategoryModule {}
π If these patterns are new to you, give this article a clap so others can discover these techniques too!
Bonus Tips for Angular Performance π¨
- Use OnPush Change Detection with scoped services for better performance
- Implement proper service disposal in your modules
- Consider using Angular's tree-shakable providers for truly optional services
- Monitor your bundle size β scoped services help keep chunks smaller
Your turn: What's one performance tip you wish you'd known earlier? Share it in the comments! π
Recap: Your Service Scoping Cheat Sheet π
β Use Root-Level Services For:
- Authentication and authorization
- Global configuration
- App-wide state management
- Core utilities (logging, error handling)
β Use Module-Level Services For:
- Feature-specific business logic
- Data services for specific domains
- Services shared within a feature module
β Use Component-Level Services For:
- Temporary state management
- Component-specific calculations
- Services that need fresh instances
β Avoid Root-Level Services For:
- Heavy data loading services
- Feature-specific utilities
- Services that aren't needed app-wide
What's Next? π
Now that you know how to scope your services properly, your Angular apps will be:
- Faster at startup (no unnecessary service initialization)
- Smaller in memory footprint
- More maintainable with clear service boundaries
- Better performing with lazy loading
Action points for you:
- Audit your current services β how many are unnecessarily root-level?
- Refactor one service this week using the patterns above
- Add proper unit tests for your scoped services
- Monitor your app's performance before and after
Let's Connect! π€
π¬ What did you think?
Have you been over-using root-level services too? Or do you have a different approach to service scoping? I'd love to hear your experience in the comments!
π Found this helpful?
If this article saved you from a performance headache, smash that clap button (or give it 5 claps!) so other developers can find it too.
π¬ Want more Angular tips like this?
I share practical Angular insights every week. Follow me here on Medium or connect with me on LinkedIn [@sadhana_dev] for more developer tips that actually work.
π₯ What should I write about next?
Vote in the comments:
- A) Advanced Angular Testing Patterns
- B) Angular Performance Optimization Deep Dive
- C) Building Scalable Angular Architecture
Let's build better Angular apps together! π
π Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- πΌ LinkedIn β Letβs connect professionally
- π₯ Threads β Short-form frontend insights
- π¦ X (Twitter) β Developer banter + code snippets
- π₯ BlueSky β Stay up to date on frontend trends
- π GitHub Projects β Explore code in action
- π Website β Everything in one place
- π Medium Blog β Long-form content and deep-dives
- π¬ Dev Blog β Free Long-form content and deep-dives
- βοΈ Substack β Weekly frontend stories & curated resources
- π§© Portfolio β Projects, talks, and recognitions
- βοΈ Hashnode β Developer blog posts & tech discussions
π If you found this article valuable:
- Leave a π Clap
- Drop a π¬ Comment
- Hit π Follow for more weekly frontend insights
Letβs build cleaner, faster, and smarter web apps β together.
Stay tuned for more Angular tips, patterns, and performance tricks! π§ͺπ§ π
β¨ Share Your Thoughts To π£ Set Your Notification Preference
Top comments (0)