Performance insights from refactoring our custom Block Registration in WooCommerce

We often encounter solutions that grow organically over time, accumulating complexity as new features and edge cases emerge. Recently, I refactored WooCommerce’s product-specific block registration system, moving from a function-based approach to a more robust, pattern-oriented solution. Along the way, I gained insights into optimizing performance and uncovered practical tips for working with Gutenberg blocks in React—here’s what I learned.

The challenge: Too many block subscriptions causing performance bottlenecks

Our original system used registerBlockSingleProductTemplate, a function that handled block registration for product-related blocks. Certain blocks like the Product Price block require a product-specific context to function – displaying a price makes sense in a product template but not on unrelated ones like a post. Thus, we needed to register blocks only in relevant templates and unregister them elsewhere for efficiency and accuracy.

The existing implementation created new store subscriptions for every block registered, resulting in O(n) subscriptions where n is the number of blocks. Each subscription would independently:

  • Watch for template changes
  • Handle block registration/unregistration
  • Manage ancestor constraints
  • Track registration attempts

This meant that with 10 blocks, we had 10 separate subscriptions, all watching for the same template changes and potentially trying to register/unregister blocks simultaneously. Not only was this inefficient, but it also led to race conditions and unreliable behavior.

// The old approach created a subscription for EACH block
subscribe(() => {
  // Each subscription independently:
  const editSiteStore = select('core/edit-site');  // Multiple store selects
  const templateId = parseTemplateId(editSiteStore?.getEditedPostId()); // Multiple template checks
  
  if (templateId changed) {
    unregisterBlockType(blockName);  // Race conditions possible here
    registerBlockType(blockName, settings); // And here
  }
}, 'core/edit-site');

The performance impact was significant. Our testing showed that:

  • The old registerBlockSingleProductTemplate executed 4,550 subscription callbacks during initial page load of the Single Product template in the Site Editor, with an accumulated execution time of 2.47s.
  • Even worse, because the old code never unsubscribed, these callbacks continued to execute when interacting with the Site Editor canvas.

It also had several other limitations:

  • Error handling and type safety could be improved
  • The API diverged from WordPress core conventions

The system worked most of the time, but could behave unpredictably.

The solution: Building a centralized manager to reduce callbacks and improve speed

The refactor introduced a new BlockRegistrationManager class implementing the Singleton pattern, along with a more intuitive registerProductBlockType function. 

Expand to see the improved approach to handling subscriptions
// The improved approach uses a single subscription managed by BlockRegistrationManager
class BlockRegistrationManager {
  private currentTemplateId: string | undefined;
  private blocks: Map<string, ProductBlockConfig> = new Map();
 
  private initializeSubscriptions(): void {
    // Single subscription to detect editor context
    const unsubscribe = subscribe(() => {
      const editSiteStore = select('core/edit-site');
      const editPostStore = select('core/edit-post');
 
      // Early return if stores aren't ready
      if (!editSiteStore && !editPostStore) {
        return;
      }
 
      // Site Editor Context
      if (editSiteStore) {
        // Unsubscribe once we know our context
        unsubscribe();
 
        // Set up single template change listener
        subscribe(() => {
          const previousTemplateId = this.currentTemplateId;
          this.currentTemplateId = this.parseTemplateId(
            editSiteStore.getEditedPostId()
          );
 
          if (previousTemplateId !== this.currentTemplateId) {
            // Handle all blocks in a single place
            this.handleTemplateChange(previousTemplateId);
          }
        }, 'core/edit-site');
      }
    });
  }
 
  private handleTemplateChange(previousTemplateId: string | undefined): void {
    // Only process blocks if we're transitioning to/from single-product template
    const isTransitioningToOrFromSingleProduct =
      this.currentTemplateId?.includes('single-product') ||
      previousTemplateId?.includes('single-product');
 
    if (!isTransitioningToOrFromSingleProduct) {
      return;
    }
 
    // Process all blocks in a controlled manner
    this.blocks.forEach((config) => {
      this.unregisterBlock(config);
      this.registerBlock(config);
    });
  }
}

Key improvements include:

  1. Performance: Single store subscription for all blocks instead of one per block, significantly reducing overhead
    • Executes only 24 subscription callbacks (down from 4,550)
    • Reduces execution time to 0.63s (down from 2.47s)
    • Properly handles unsubscription to prevent callback accumulation
  2. Architecture: Implemented Singleton pattern for consistent block management and better type safety
  3. Developer Experience: New API that mirrors WordPress core’s registerBlockType with improved error handling
  4. Reliability: More robust handling of edge cases and context-specific constraints

Key takeaways for extending the block editor

Here are some short, actionable tips when working with React which you should be conscious of.

  1. Spotting Performance Bottlenecks: Lagging renders, excessive re-renders, or slow interactions are key symptoms of inefficiency. Use React DevTools Profiler to pinpoint costly components and check for unnecessary state subscriptions or API calls.
  2. Unsubscribing from Store Updates: Unsubscribe when your block or component no longer needs real-time updates. This prevents memory leaks and wasted cycles—crucial for dynamic systems like Gutenberg.
  3. When to Abstract: If you’re reusing logic across blocks or see duplicate patterns, it’s time for an abstraction. A shared hook, utility, or component can simplify maintenance—just ensure it’s flexible for future cases.

You can check out the full refactor in these PRs if you want to dive into the nitty-gritty details. Oh, and here’s the file.

A version of this was originally published on Tom’s blog.


Leave a Reply

Your email address will not be published. Required fields are marked *