Master Gutenberg dynamic block rendering in PHP. Learn to implement render_callback, optimize server-side performance, and cache output for high-traffic sites.
Previously in this course, we explored Block API v2 Essentials and how to manage complex editor experiences in Custom Gutenberg Block Controls. While those lessons focused on the React-based editor interface, this lesson shifts focus to the server. We will implement dynamic block rendering, ensuring your blocks remain performant even when they pull live data.
In Gutenberg, a "static" block saves its HTML directly into the post content. A "dynamic" block, however, saves only its attributes. At render time, WordPress executes a PHP callback function to generate the HTML.
This architecture is essential for our Knowledge Base plugin. If we saved the list of "Latest Articles" as static HTML, that list would become stale the moment a new article is published. By using a render_callback, we ensure the block always displays fresh, live data while keeping the post content clean.
To make a block dynamic, you must define the render_callback property in your block.json. This tells WordPress to delegate the rendering to a specific PHP function instead of looking for a save() function in your JavaScript.
In your block.json, omit the save property and add render_callback:
JSON{ "name": "kb/latest-articles", "attributes": { "limit": { "type": "number", "default": 5 } }, "render_callback": "KB\\Blocks\\render_latest_articles" }
Your callback receives the block's attributes as an array. Use these to fetch data, but always wrap your logic in a performance-focused container.
PHPnamespace KB\Blocks; function render_latest_articles(array $attributes): string { $limit = $attributes['limit'] ?? 5; $cache_key = 'kb_latest_articles_' . md5(serialize($attributes)); #6A9955">// Attempt to retrieve from cache $output = wp_cache_get($cache_key, 'kb_blocks'); if (false === $output) { $articles = get_posts([ 'post_type' => 'kb_article', 'posts_per_page' => $limit, ]); ob_start(); foreach ($articles as $post) { echo "<li><a href='" . esc_url(get_permalink($post)) . "'>" . esc_html($post->post_title) . "</a></li>"; } $output = "<ul>" . ob_get_clean() . "</ul>"; #6A9955">// Cache for 1 hour wp_cache_set($cache_key, $output, 'kb_blocks', HOUR_IN_SECONDS); } return $output; }
Dynamic blocks execute on every page load. If your logic involves heavy database queries (like complex WP_Query arguments or custom SQL), you are essentially multiplying the load on your database.
Follow these rules for production-grade rendering:
wp_cache_set to store the generated HTML. Use a unique key based on the block attributes so that different configurations don't overwrite each other.save_post hook.wp_kses_post() or specific escaping functions on all dynamic output. Even if you trust the source data, the rendering pipeline must be bulletproof against injection.Caching is only useful if it eventually clears. In the Knowledge Base plugin, if an article is updated, your "Latest Articles" block should reflect that immediately.
Use the save_post hook to invalidate your specific block cache group:
PHPadd_action('save_post_kb_article', function() { #6A9955">// This clears all cached blocks in our group wp_cache_delete('kb_latest_articles', 'kb_blocks'); });
Note: If you need granular invalidation, store individual keys in a transient or an array option that you can purge selectively.
render_callback.wp_cache_set pattern provided above and observe the drop in database queries on subsequent page loads.render_callback: Forgetting to register the function in block.json will result in an empty block on the frontend.esc_ functions: Since you are building the HTML string manually in PHP, it is incredibly easy to forget to escape the content. Use wp_kses_post on the final output string before returning it.get_posts or WP_Query inside the render function in a way that recurses if the post being queried contains the same block (though WordPress has internal protections, it's a structural risk).render_callback to generate HTML at runtime, ensuring your plugin data is always current.By moving rendering to the server, we've successfully decoupled our UI from the static post content, allowing for a more modular and performant architecture.
Up next: We will dive into Advanced State Persistence, where we explore how to synchronize your block's complex internal states with local browser storage and server-side databases for a seamless UX.
Master enterprise-grade caching by implementing tag-based transient invalidation and multi-site synchronization to ensure your plugin scales under high load.
Read moreLearn how to implement Transients and the Object Cache to slash database overhead in your custom WordPress tables, ensuring your plugins scale at speed.
Dynamic Block Rendering