Performance is crucial for editor applications. The Block Editor monitors key metrics and implements strategies to maintain responsiveness as content grows.
Key Metrics
Three primary metrics are tracked:
Loading Time
The time from page request to first block render:
- Server response time
- Time to first paint (FP)
- Time to first contentful paint (FCP)
- DOM content load complete
- Load complete
- First block render
Typing Time
Browser response time while typing in the editor. This measures the delay between keypress and visual update.
Block Selection Time
Response time when selecting or inserting blocks. Since inserting a block is equivalent to selecting it, this metric covers both operations.
Monitor these metrics during development to catch performance regressions early.
Data Module Async Mode
The Block Editor uses Redux for state management through the @wordpress/data package. As the number of blocks grows, global state updates can cause performance issues.
The Problem
With synchronous rendering:
- Every state change triggers updates to all subscribed components
- Long posts with many blocks cause UI lag
- Typing becomes sluggish as component count increases
The Solution
Async Mode enables selective synchronous rendering:
// Selected block renders synchronously for instant feedback
<BlockListBlock clientId={ selectedBlock } isSynchronous={ true } />
// Other blocks render asynchronously when browser is idle
<BlockListBlock clientId={ otherBlock } isSynchronous={ false } />
How it works:
- The selected block updates synchronously for immediate response
- All other blocks update asynchronously during browser idle time
- Editor stays responsive regardless of content length
This optimization is based on the principle that editing one block rarely affects others, so most updates can be deferred.
Running Benchmarks Locally
Compare performance across branches:
./bin/plugin/cli.js perf trunk v8.1.0 v8.0.0
The tool:
- Prepares test environments for each branch
- Runs performance tests in each environment
- Computes medians for all metrics
- Generates a comparison report
Test Environment Structure
├── tests/packages/e2e-tests/specs/performance/*
│ Performance test suites
│
├── tests/test/emptytheme
│ Theme for test environment
│
├── envs/branch1/.wp-env.json
│ Environment config for branch1
│
├── envs/branch1/plugin
│ Built Gutenberg plugin for branch1
│
└── envs/branchX
Additional branch environments
Test Process
For each branch:
- Start the wp-env environment
- Run performance test suite
- Stop the environment
- Record results
- Repeat for remaining branches
- Calculate and display medians
Use identical test and environment versions across branches. Only the Gutenberg plugin version should differ.
CodeVitals Tracking
Performance results are tracked on CodeVitals for the Gutenberg project.
Handling CI Variability
GitHub CI resources vary between runs. To ensure consistent tracking:
- Each trunk commit is compared to a fixed reference commit
- Relative differences remain consistent despite environment changes
- Trends are reliable even with varying absolute numbers
Updating the Reference Commit
Update when WordPress version requirements change:
# .github/workflows/performance.yml
reference-commit: 'abc123'
The reference commit must:
- Be compatible with the new WordPress version
- Already be tracked on CodeVitals for all metrics
- Have a passing performance job
Choose a recent trunk commit with passing tests as your reference commit.
Optimization Strategies
Reduce Component Re-renders
Use selectors efficiently:
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
// Bad: Selects entire block object, causes re-render on any change
const block = useSelect(
( select ) => select( blockEditorStore ).getBlock( clientId ),
[ clientId ]
);
// Good: Select only needed properties
const { blockName, attributes } = useSelect(
( select ) => {
const block = select( blockEditorStore ).getBlock( clientId );
return {
blockName: block?.name,
attributes: block?.attributes,
};
},
[ clientId ]
);
Memoize Expensive Calculations
import { useMemo } from 'react';
function BlockEdit( { attributes } ) {
// Expensive calculation only runs when attributes change
const processedData = useMemo(
() => expensiveOperation( attributes ),
[ attributes ]
);
return <div>{ processedData }</div>;
}
import { useDebounce } from '@wordpress/compose';
function SearchControl() {
const [ searchTerm, setSearchTerm ] = useState( '' );
// Debounce expensive search operation
const debouncedSearch = useDebounce(
( value ) => performSearch( value ),
500
);
const handleChange = ( value ) => {
setSearchTerm( value );
debouncedSearch( value );
};
}
Lazy Load Heavy Components
import { Suspense, lazy } from 'react';
const HeavyComponent = lazy( () => import( './HeavyComponent' ) );
function MyBlock() {
return (
<Suspense fallback={ <Spinner /> }>
<HeavyComponent />
</Suspense>
);
}
Best Practices
- Profile performance before and after changes
- Use browser DevTools Performance tab to identify bottlenecks
- Minimize selector dependencies in
useSelect
- Avoid anonymous functions in render methods
- Use
React.memo for components that rarely change
- Keep component trees shallow
import { useEffect } from 'react';
function MyComponent() {
useEffect( () => {
const startTime = performance.now();
// Perform operation
expensiveOperation();
const duration = performance.now() - startTime;
console.log( `Operation took ${ duration }ms` );
}, [] );
}
Use the Profiler to identify slow components:
- Open React DevTools
- Switch to Profiler tab
- Click record
- Perform actions in editor
- Stop recording
- Analyze component render times
Additional Resources