DEV Community

Cover image for The Angular Service Scoping Mistake That's Killing Your App Performance (And How to Fix It)
Rajat
Rajat

Posted on

The Angular Service Scoping Mistake That's Killing Your App Performance (And How to Fix It)

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

Looks innocent, right? But here's what's happening under the hood:

  1. Your service loads ALL products the moment your app starts
  2. It stays in memory even when users are on completely unrelated pages
  3. 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) {}
}

Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

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();
  }
}

Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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(/*...*/);
  }
}

Enter fullscreen mode Exit fullscreen mode
// βœ… 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 {}

Enter fullscreen mode Exit fullscreen mode

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
  });
});

Enter fullscreen mode Exit fullscreen mode

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();
  }
}

Enter fullscreen mode Exit fullscreen mode

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$)
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

3. Service Factory Pattern

export function createCategoryService(category: string) {
  return new CategoryService(category);
}

@NgModule({
  providers: [
    {
      provide: CategoryService,
      useFactory: createCategoryService,
      deps: ['categoryName']
    }
  ]
})
export class DynamicCategoryModule {}

Enter fullscreen mode Exit fullscreen mode

πŸ‘ If these patterns are new to you, give this article a clap so others can discover these techniques too!


Bonus Tips for Angular Performance πŸ’¨

  1. Use OnPush Change Detection with scoped services for better performance
  2. Implement proper service disposal in your modules
  3. Consider using Angular's tree-shakable providers for truly optional services
  4. 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:

  1. Audit your current services β€” how many are unnecessarily root-level?
  2. Refactor one service this week using the patterns above
  3. Add proper unit tests for your scoped services
  4. 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)