Contents
Why code-splitting blocks matters for the WordPress editor
Problem: Many block plugins bundle every blocks editor code into a single large JavaScript file. The Gutenberg editor must parse and execute that file on editor load, which increases time-to-interactive, memory usage, and CPU on lower-end devices.
Goal: Load only the JavaScript that the editor actually needs at any given time — initial editor UI and the minimal registration code — and defer or lazy-load heavy editor UI (controls, chart libraries, large React components) until the block is selected or used. The result: faster editor load, snappier typing/selection, and better perceived performance.
High-level strategies
- Dynamic imports (on-demand loading): Keep a small registration/manifest script that registers block metadata but lazy-loads the heavy edit component with import() when the block is selected or first edited.
- Multiple webpack entry points (per-block bundles): Build separate editor scripts per block so the editor only enqueues the blocks editor bundle when the block is present/needed.
- Vendor splitting and shared chunks: Extract large third-party libraries into separate vendor chunks to avoid duplicating code across blocks and to improve cacheability.
- Externalizing WordPress packages: Use dependency-extraction to keep @wordpress/ packages external (so React/wpa packages are not re-bundled), drastically reducing bundle sizes.
- Lazy load heavy UI only when necessary: Load inspector or advanced controls only when the inspector is opened or the block is selected, not on initial load.
Prerequisites and recommended toolset
- Node.js >= 14 (or current LTS).
- @wordpress/scripts (recommended) or a custom webpack config.
- Familiarity with registerBlockType (block.json block registration) and basic webpack configuration.
- Use modern bundlers that support dynamic import() (webpack supports it out-of-the-box).
Strategy A — Dynamic import of the edit component (recommended, least invasive)
This approach keeps a small registration script but defers loading heavy edit UI until the block is actually being used/selected. It is robust and works well with a single bundle build system.
Why it helps
- The editor still needs to know the block exists, but it does not need the heavy edit component code until user tries to edit the block.
- Works well for blocks that are rarely edited or blocks that have heavy editor-only libraries (charts, complex WYSIWYG, code editors, etc.).
Example: register block and lazy-load edit
import { registerBlockType } from @wordpress/blocks import { Spinner } from @wordpress/components import { useState, useEffect } from @wordpress/element // Minimal block registration file (small bundle) registerBlockType( my-plugin/lazy-block, { apiVersion: 2, title: Lazy Block, category: widgets, edit: ( props ) => { const [ EditComponent, setEditComponent ] = useState( null ) useEffect( () => { let mounted = true // dynamic import: bundle containing ./edit will be created as a separate chunk import( ./edit ) .then( ( module ) => { if ( mounted ) { // setEditComponent to the default export of the module setEditComponent( () => module.default ) } } ) .catch( ( err ) => { // handle error gracefully in the editor // console.error( Failed to load edit component, err ) } ) return () => { mounted = false } }, [] ) if ( ! EditComponent ) { // lightweight placeholder while the real editor code loads return} return }, save: () => { return null // dynamic/supported server-side or static save depending on the block }, } )
Load only when the block is selected (more advanced)
Improve perceived performance further by only loading the heavy edit component when the block is actively selected (not for every block instance present in the content).
import { registerBlockType } from @wordpress/blocks import { useState, useEffect } from @wordpress/element import { useSelect } from @wordpress/data registerBlockType( my-plugin/on-demand-block, { apiVersion: 2, title: On-Demand Block, edit: ( props ) => { const { clientId } = props const isSelected = useSelect( ( select ) => select( core/block-editor ).isBlockSelected( clientId ), [ clientId ] ) const [ Component, setComponent ] = useState( null ) useEffect( () => { let mounted = true if ( isSelected ! Component ) { import( ./edit ) .then( ( module ) => { if ( mounted ) setComponent( () => module.default ) } ) .catch( () => { // handle gracefully } ) } return () => { mounted = false } }, [ isSelected ] ) if ( ! Component ) { // lightweight placeholder for unselected block instances returnClick to edit block} return}, save: () => null, } )
Notes and tips
- Show a minimal placeholder while the chunk is being fetched to keep the editor responsive.
- Guard dynamic imports with graceful error handling to avoid breaking the editor when chunk loading fails.
- Consider preloading the chunk if you detect the user is likely to open the block soon (for example, when they click to insert the block from a modal). See Preload and prefetch below.
Strategy B — Multiple webpack entry points (per-block bundles)
Create separate editor bundles per block. This allows WordPress to register/enqueue fewer scripts in the editor, or to only enqueue a blocks script when the block is needed (with proper build PHP registration).
Webpack example: per-block entry points and splitChunks
const defaultConfig = require( @wordpress/scripts/config/webpack.config ) const path = require( path ) const DependencyExtractionWebpackPlugin = require( @wordpress/dependency-extraction-webpack-plugin ) module.exports = { ...defaultConfig, entry: { block-a: path.resolve( process.cwd(), src/block-a/index.js ), block-b: path.resolve( process.cwd(), src/block-b/index.js ), // add more blocks here }, output: { path: path.resolve( process.cwd(), build ), filename: [name].js, chunkFilename: [name].[contenthash].js, }, optimization: { splitChunks: { chunks: all, cacheGroups: { vendor: { test: /[/]node_modules[/]/, name: vendor, chunks: all, }, }, }, runtimeChunk: { name: runtime }, }, plugins: [ new DependencyExtractionWebpackPlugin(), // externalize WP packages (wp.element, wp.blocks, etc.) ], }
Why externalize WP packages?
- WordPress packages like wp.element, wp.i18n, wp.components are already loaded by the editor. Bundling them duplicates code and increases bundle size.
- The dependency-extraction plugin replaces imports from @wordpress/ with external window.wp references and emits an index.asset.php with dependencies version that PHP can use when registering scripts.
Registering the generated files in PHP
After building per-block assets, you need to register the scripts/styles with their dependency info. A common approach is to use the generated index.asset.php files.
// Example: build/block-a/index.asset.php contains array(dependencies => [...], version => ...) // and build/block-a/index.js is the built file. function my_plugin_register_blocks() { blocks = array( block-a, block-b ) foreach ( blocks as block ) { dir = __DIR__ . /build/{block} if ( ! file_exists( {dir}/index.asset.php ) ) { // dev/build step not run or file missing continue } asset = require {dir}/index.asset.php // Register the editor script for this block wp_register_script( my-plugin-{block}-editor, plugins_url( build/{block}/index.js, __FILE__ ), asset[dependencies], asset[version], true ) // Optionally register a frontend script/style if generated if ( file_exists( {dir}/style-index.css ) ) { wp_register_style( my-plugin-{block}-style, plugins_url( build/{block}/style-index.css, __FILE__ ), array(), filemtime( {dir}/style-index.css ) ) } // Register block from metadata: block.json should exist in build/{block} register_block_type( {dir}/block.json ) } } add_action( init, my_plugin_register_blocks )
Notes
- If you use register_block_type( path_to_block_json ), WordPress will look at block.json and automatically enqueue editor_script handles — ensure your block.json correctly references the editor script handle generated in build.
- Using separate entry points and per-block bundles is more work during build but offers the most control over what loads when.
Optimizing chunking vendor splitting
- Use splitChunks to separate large node_modules into vendor chunks so multiple blocks can share them and benefit from long-term caching.
- Set filename and chunkFilename to include contenthash so you get cache-busting when the chunk changes: filename: [name].[contenthash].js.
- Use runtimeChunk to separate webpack runtime to avoid invalidating shared chunks unnecessarily.
Mapping chunks to WordPress enqueues (manifest)
If you use contenthash in names, you must map hashed filenames to script handles at PHP runtime. Use a manifest JSON or the index.asset.php plugin that writes a mapping to disk during build. Options:
- Emit a manifest.json in webpack build (via webpack-manifest-plugin) and load it in PHP to find the correct hashed filenames.
- Use the WordPress dependency-extraction plugin output and place built files in per-block folders so register_block_type_from_metadata can discover them.
Preload and prefetch strategies
Preloading can trade initial JS parse time for smaller latency to open a specific block. Use prefetch for lower-priority chunks and preload for high-priority ones.
Example approach: when the user opens the block inserter and hovers or focuses a block card, programmatically add a link rel=preload or link rel=prefetch for that blocks chunk. This is optional and can improve UX for predictable flows.
Measure, test and validate
- Measure editor load time before and after changes. Use Chrome DevTools Performance panel and Lighthouse to measure main-thread blocking time and Time to Interactive.
- Use React DevTools profiler if an interactive block is slow when selected.
- Test on low-end devices and throttled CPU network to reveal real world improvements.
- Test accessibility and keyboard flows — ensure lazy-loading doesn’t break keyboard navigation or focus order.
Common pitfalls and how to avoid them
- Chunk fails to load: handle import() errors and show a fallback UI. Network errors or CSP policies can break dynamic import.
- Missing dependencies: ensure the dependency-extraction webpack plugin is configured so @wordpress packages are external and declared as dependencies in index.asset.php so WordPress loads them first.
- Cache invalidation: use proper hashing (contenthash) for chunk files and maintain a reliable manifest to map handles to hashed files.
- SEO/frontend impact: ensure editor-only code remains editor-only avoid accidentally loading heavy editor-only scripts on the frontend by correctly using block.json editorScript vs script fields.
- Internationalization: If your edit components use translations, ensure the dynamic chunk does not miss i18n initialization. Keep wp-i18n textdomain initialization in the main registration script or ensure translations are available when the dynamic chunk runs.
Advanced: extract runtime vendor chunk examples and dependency-extraction plugin usage
const DependencyExtractionWebpackPlugin = require( @wordpress/dependency-extraction-webpack-plugin ) const { merge } = require( webpack-merge ) const base = require( @wordpress/scripts/config/webpack.config ) module.exports = merge( base, { entry: { block-a: ./src/block-a/index.js, block-b: ./src/block-b/index.js }, output: { filename: [name].[contenthash].js, chunkFilename: [name].[contenthash].js, publicPath: /wp-content/plugins/my-plugin/build/, }, optimization: { splitChunks: { chunks: all, cacheGroups: { defaultVendors: { test: /[/]node_modules[/]/, name: vendors, chunks: all, priority: -10, }, }, }, runtimeChunk: { name: runtime, }, }, plugins: [ new DependencyExtractionWebpackPlugin({ injectPolyfill: false }), ], } )
Example: Using a heavy third-party library only in edit via dynamic import
Imagine a block that uses Chart.js only in the editor. Do not import Chart.js at the top-level import it inside edit when needed:
// src/my-chart-block/edit.js import { useEffect, useRef } from @wordpress/element export default function Edit( props ) { const canvasRef = useRef() useEffect( () => { let chart import( chart.js ).then( ( ChartJs ) => { // ChartJs default export or named exports depending on package chart = new ChartJs.Chart( canvasRef.current.getContext( 2d ), { type: bar, data: { / ... / }, } ) } ) return () => { if ( chart ) chart.destroy() } }, [] ) return }
Checklist to implement code-splitting for your block plugin
- Decide strategy: dynamic import for minimal changes, per-block bundles for fine-grained control.
- Ensure @wordpress packages are externalized with dependency-extraction plugin to reduce bundle size.
- Wrap heavy edit UI in dynamic imports and provide a lightweight placeholder or spinner.
- If using per-block bundles, produce index.asset.php per bundle and register with register_block_type or wp_register_script.
- Use splitChunks to create shared vendor chunks and runtime chunk for better caching.
- Add error handling for failed chunk loads and fallback UI in the editor.
- Measure editor performance before and after. Test on throttled CPU and low-end devices.
- Maintain a manifest mapping hashed files to enqueued handles if using contenthash.
Further reading and tools
- WordPress Block Editor Handbook — for block.json and registration conventions.
- Dependency Extraction Webpack Plugin — plugin to externalize WordPress packages.
- Webpack Code Splitting Guide
- Chrome DevTools Performance — for profiling editor main-thread tasks.
Final notes (practical recommendations)
Start with dynamic imports: they are the easiest to adopt and provide immediate improvements without changing your build to multiple entry points.
Combine approaches: for best results, use dynamic imports for per-block heavy editor UI and multi-entry builds splitChunks for library-level optimization and caching improvements.
Always test: code splitting changes the network and execution characteristics of your plugin validate that user flows remain smooth, error cases are handled, and translations and dependencies are still loaded correctly in the editor.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |