Avoid Unnecessary Rendering

Avoiding Unnecessary Rendering is a core principle for improving application responsiveness and user experience in modern frontend frameworks like React and Angular. Although their underlying implementations differ, both rely on the Virtual DOM mechanism to optimize rendering operations. When users perform interactions, data updates, or state changes, if a large number of nodes within the component tree are recalculated and pushed to the browser for actual rendering, it results in noticeable lag and latency.

The key to understanding rendering costs lies in recognizing that every modification to the real DOM (such as insertion, deletion, or style changes) triggers the browser's Reflow and Repaint, which can even cause full-page flicker. Therefore, the primary goal of optimization is to ensure that only components and their subtrees that truly need updating undergo this process.
React automatically handles most rendering logic through its powerful Virtual DOM mechanism. When a parent component's state changes, React generates a new Virtual DOM tree (New Virtual DOM) and performs a deep comparison (Diffing Algorithm) with the previous old Virtual DOM tree. Only nodes that have changed are marked as "dirty" and applied to the real DOM.

To further reduce unnecessary rendering, developers should fully leverage the state management mechanisms provided by React:

  • Avoid directly mutating objects or arrays in render functions
    If a reference is mutated directly within a component (e.g., this.state.count++), React cannot detect the change, leading to the reuse of old state and potentially causing an infinite loop. The correct approach is to always create new references (e.g., setState(count + 1)).

  • Reasonably use pure functions and immutable data
    Encapsulate data processing logic in pure functions to ensure consistent output for identical inputs. This helps React accurately determine which data has changed. For example:

// ❌ Incorrect Example: Directly modifying object properties results in no reference change
function incrementCount(count) {
  return count + 1; 
}

// ✅ Correct Approach: Return new values to ensure a new numeric reference is created on each call (for primitives)
// Or create a copy before assignment for complex objects
  • Use React.memo for Higher-Order Component optimization
    For components with simple logic and low data change frequency, use memo to wrap them. It caches the rendering result and only re-executes render if child props or keys change:
import React from 'react';

const ExpensiveComponent = React.memo(({ data }) => {
  // ...Complex calculation logic...
});

Angular: Leveraging Change Detection Strategies and Data Flow Control

Angular also relies on the Virtual DOM, but its rendering mechanism is driven by "Change Detection." By default, Angular adopts a two-way binding and event-driven change detection pattern, listening to @Input, @Output, and $changeDetectionRef.onChanges() callbacks on components to trigger updates.

However, this full-check strategy can lead to performance bottlenecks in large applications. The key to optimizing Angular applications lies in:

  • Leveraging Data Streams instead of direct assignment
    If parent component data changes frequently but the child component's UI logic does not change accordingly, forcing notification to the change detector leads to unnecessary rendering. It is recommended to manage state using RxJS observables (such as BehaviorSubject, AsyncPipe):
// ❌ Inefficient approach: Direct assignment may trigger unnecessary deep checks
this.userData = { ...oldData }; 

// ✅ Efficient approach: Use AsyncPipe; Angular automatically handles change notifications in the data stream
@Component({ template: '' })
class ChildComponent {
  data$ = this.parentService.data$.pipe(
    switchMap((data) => of(data)) // Ensures each update triggers child component check, but logic is clearer
  );

  ngOnInit() {
    this.subscription = this.data$.subscribe({
      next: (val) => console.log(val),
      error: () => {} 
    });
  }
}
  • Enable Lazy Loading and OnPush Strategy
    Angular 12+ introduced the OnPush change detection strategy. In components marked with ChangeDetectorRef.OnPush(), checks are triggered only when input properties or event outputs change. This is highly effective for UI modules that serve primarily as presentation layers with infrequent data updates:
@Component({ 
  selector: 'app-static-card', 
  changeDetection: ChangeDetectionStrategy.OnPush, // Key optimization configuration
})
export class StaticCardComponent { ... }
  • Avoid executing side effects in templates
    Angular checks directives during every rendering cycle. If complex calculations are performed in ngOnInit, ngAfterViewInit, or template strings, it may lead to unnecessary repaints even if data hasn't changed. Extract all business logic into Services and notify components for updates via events when needed.

Summary and Best Practice Recommendations

Whether using React or Angular, "avoiding unnecessary rendering" is not just a technical means of performance optimization but also a philosophical embodiment of building maintainable code. It requires developers to deeply understand the data flow mechanisms and change detection principles of the framework to trigger correct updates at the right time.

In actual development, follow these principles:

  1. Minimize Data Dependencies: Ensure components subscribe or listen only to data sources they truly need to change.
  2. Prioritize Immutable Data Structures: This significantly improves the accuracy of the Diff algorithm.
  3. Leverage Framework-Provided Optimization Tools: Such as React's memo and Angular's OnPush, which are efficient solutions verified by the community.

By implementing these strategies, we can significantly reduce CPU usage and memory consumption, ensuring that applications remain smooth and silky even in complex interaction scenarios.