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:
- 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
- Architecture: Implemented Singleton pattern for consistent block management and better type safety
- Developer Experience: New API that mirrors WordPress core’s
registerBlockType
with improved error handling - 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.
- 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.
- 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.
- 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.
registerBlockSingleProductTemplate
: Refactor to use aBlockRegistrationManager
class and Singleton pattern #53788- Rename
registerBlockSingleProductTemplate
toregisterProductBlockType
#53872 registerBlockSingleProductTemplate
: Fix template switching #53874registerProductBlockType
: Update signature and usages #53895
A version of this was originally published on Tom’s blog.
Leave a Reply