diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md
index b051f1aaae5efb..8754f7a0f7cba6 100644
--- a/packages/block-editor/CHANGELOG.md
+++ b/packages/block-editor/CHANGELOG.md
@@ -9,6 +9,10 @@
- Inherited Global Styles now resolve the `link` element (`styles.elements.link`) for whole-block link blocks — those that render as a link — so their inspector controls reflect inherited link styles, mirroring the `button` element treatment for `core/button`. Covers `core/read-more`, `core/loginout`, `core/post-navigation-link`, `core/query-pagination-next`, `core/query-pagination-previous`, `core/query-pagination-numbers`, `core/comments-pagination-next`, `core/comments-pagination-previous`, `core/comments-pagination-numbers`, `core/comment-edit-link`, `core/comment-reply-link`, and `core/post-comments-link` [#80607](https://github.com/WordPress/gutenberg/pull/80607)).
- Add support for the `blockStatesEnabled` editor setting, which hides state controls for blocks when set to `false` ([#80956](https://github.com/WordPress/gutenberg/pull/80956)).
+### Performance
+
+- `ListView`: Collapse the placeholder rows that stand in for blocks outside of the render window into a single spacer row per run, instead of rendering a `
`/`| ` pair for every block. On a post with 1000 top-level blocks this removes ~1900 elements (about 60% of the List View's DOM and nearly half of the document's elements), which cuts the style recalculation and layout work done when the List View opens ([#80953](https://github.com/WordPress/gutenberg/pull/80953)).
+
### Internal
- `ListView`: Reimplement the Firefox description-recomputation workaround in `AriaReferencedText` by keying the element on its text, so React replaces it instead of updating the existing text node in place ([#80929](https://github.com/WordPress/gutenberg/pull/80929).
diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js
index 5df7134e125cdb..b121f05367fea5 100644
--- a/packages/block-editor/src/components/list-view/branch.js
+++ b/packages/block-editor/src/components/list-view/branch.js
@@ -5,7 +5,7 @@ import {
__experimentalTreeGridRow as TreeGridRow,
__experimentalTreeGridCell as TreeGridCell,
} from '@wordpress/components';
-import { memo, useRef } from '@wordpress/element';
+import { memo } from '@wordpress/element';
import { AsyncModeProvider, useSelect } from '@wordpress/data';
/**
@@ -14,7 +14,11 @@ import { AsyncModeProvider, useSelect } from '@wordpress/data';
import { Appender } from './appender';
import ListViewBlock from './block';
import { useListViewContext } from './context';
-import { getDragDisplacementValues, isClientIdSelected } from './utils';
+import {
+ BLOCK_LIST_ITEM_HEIGHT,
+ getDragDisplacementValues,
+ isClientIdSelected,
+} from './utils';
import { store as blockEditorStore } from '../../store';
import useBlockDisplayInformation from '../use-block-display-information';
@@ -42,44 +46,21 @@ function countBlocks(
return 0;
}
const isExpanded = expandedState[ block.clientId ] ?? isExpandedByDefault;
-
- if ( isExpanded ) {
- return (
- 1 +
- block.innerBlocks.reduce(
- countReducer(
- expandedState,
- draggedClientIds,
- isExpandedByDefault
- ),
- 0
- )
- );
+ if ( ! isExpanded ) {
+ return 1;
}
- return 1;
+ return block.innerBlocks.reduce(
+ ( count, innerBlock ) =>
+ count +
+ countBlocks(
+ innerBlock,
+ expandedState,
+ draggedClientIds,
+ isExpandedByDefault
+ ),
+ 1
+ );
}
-const countReducer =
- ( expandedState, draggedClientIds, isExpandedByDefault ) =>
- ( count, block ) => {
- const isDragged = draggedClientIds?.includes( block.clientId );
- if ( isDragged ) {
- return count;
- }
- const isExpanded =
- expandedState[ block.clientId ] ?? isExpandedByDefault;
- if ( isExpanded && block.innerBlocks.length > 0 ) {
- return (
- count +
- countBlocks(
- block,
- expandedState,
- draggedClientIds,
- isExpandedByDefault
- )
- );
- }
- return count + 1;
- };
const noop = () => {};
@@ -123,8 +104,6 @@ function ListViewBranch( props ) {
draggedClientIds,
} = useListViewContext();
- const nextPositionRef = useRef();
-
if ( ! canParentExpand ) {
return null;
}
@@ -135,123 +114,159 @@ function ListViewBranch( props ) {
const blockCount = filteredBlocks.length;
// The appender means an extra row in List View, so add 1 to the row count.
const rowCount = showAppender ? blockCount + 1 : blockCount;
- nextPositionRef.current = listPosition;
+ let nextPosition = listPosition;
- return (
- <>
- { filteredBlocks.map( ( block, index ) => {
- const { clientId, innerBlocks } = block;
+ const rows = [];
- if ( index > 0 ) {
- nextPositionRef.current += countBlocks(
- filteredBlocks[ index - 1 ],
- expandedState,
- draggedClientIds,
- isExpanded
- );
- }
+ // A run of blocks outside of the render window becomes one spacer row that
+ // stands in for their height.
+ let placeholderRows = 0;
+ let placeholderKey;
- const isDragged = !! draggedClientIds?.includes( clientId );
+ // Adds the pending placeholder row, if there is one. Call before
+ // adding a real row.
+ const pushPlaceholderRow = () => {
+ if ( ! placeholderRows ) {
+ return;
+ }
+ rows.push(
+ |
+ |
+
+ );
+ placeholderRows = 0;
+ placeholderKey = undefined;
+ };
- // Determine the displacement of the block while dragging. This
- // works out whether the current block should be displaced up or
- // down, relative to the dragged blocks and the drop target.
- const { displacement, isAfterDraggedBlocks, isNesting } =
- getDragDisplacementValues( {
- blockIndexes,
- blockDropTargetIndex,
- blockDropPosition,
- clientId,
- firstDraggedBlockIndex,
- isDragged,
- } );
+ filteredBlocks.forEach( ( block, index ) => {
+ const { clientId, innerBlocks } = block;
- const { itemInView } = fixedListWindow;
- const blockInView = itemInView( nextPositionRef.current );
+ // The next sibling is offset by this block's subtree.
+ const blockListPosition = nextPosition;
+ nextPosition += countBlocks(
+ block,
+ expandedState,
+ draggedClientIds,
+ isExpanded
+ );
- const position = index + 1;
- const updatedPath =
- path.length > 0
- ? `${ path }_${ position }`
- : `${ position }`;
- const hasNestedBlocks = !! innerBlocks?.length;
+ const isDragged = !! draggedClientIds?.includes( clientId );
- const shouldExpand =
- hasNestedBlocks && shouldShowInnerBlocks
- ? expandedState[ clientId ] ?? isExpanded
- : undefined;
+ const { itemInView } = fixedListWindow;
+ const blockInView = itemInView( blockListPosition );
- // Make updates to the selected or dragged blocks synchronous,
- // but asynchronous for any other block.
- const isSelected = isClientIdSelected(
- clientId,
- selectedClientIds
- );
- const isSelectedBranch =
- isBranchSelected || ( isSelected && hasNestedBlocks );
+ const position = index + 1;
+ const updatedPath =
+ path.length > 0 ? `${ path }_${ position }` : `${ position }`;
+ const hasNestedBlocks = !! innerBlocks?.length;
- // To avoid performance issues, we only render blocks that are in view,
- // or blocks that are selected or dragged. If a block is selected,
- // it is only counted if it is the first of the block selection.
- // This prevents the entire tree from being rendered when a branch is
- // selected, or a user selects all blocks, while still enabling scroll
- // into view behavior when selecting a block or opening the list view.
- // The first and last blocks of the list are always rendered, to ensure
- // that Home and End keys work as expected.
- const showBlock =
- isDragged ||
- blockInView ||
- ( isSelected && clientId === selectedClientIds[ 0 ] ) ||
- index === 0 ||
- index === blockCount - 1;
- return (
-
- { showBlock && (
-
- ) }
- { ! showBlock && (
-
- |
-
- ) }
- { hasNestedBlocks && shouldExpand && ! isDragged && (
-
- ) }
-
- );
- } ) }
+ const shouldExpand =
+ hasNestedBlocks && shouldShowInnerBlocks
+ ? expandedState[ clientId ] ?? isExpanded
+ : undefined;
+
+ // Make updates to the selected or dragged blocks synchronous,
+ // but asynchronous for any other block.
+ const isSelected = isClientIdSelected( clientId, selectedClientIds );
+ const isSelectedBranch =
+ isBranchSelected || ( isSelected && hasNestedBlocks );
+
+ // To avoid performance issues, we only render blocks that are in view,
+ // or blocks that are selected or dragged. If a block is selected,
+ // it is only counted if it is the first of the block selection.
+ // This prevents the entire tree from being rendered when a branch is
+ // selected, or a user selects all blocks, while still enabling scroll
+ // into view behavior when selecting a block or opening the list view.
+ // The first and last blocks of the list are always rendered, to ensure
+ // that Home and End keys work as expected.
+ const showBlock =
+ isDragged ||
+ blockInView ||
+ ( isSelected && clientId === selectedClientIds[ 0 ] ) ||
+ index === 0 ||
+ index === blockCount - 1;
+
+ const showNestedBlocks = hasNestedBlocks && shouldExpand && ! isDragged;
+
+ if ( ! showBlock ) {
+ placeholderRows += 1;
+ placeholderKey ??= clientId;
+
+ // Inner blocks of an off-window block may still be in view.
+ if ( ! showNestedBlocks ) {
+ return;
+ }
+ }
+
+ pushPlaceholderRow();
+
+ // Determine the displacement of the block while dragging. This
+ // works out whether the current block should be displaced up or
+ // down, relative to the dragged blocks and the drop target.
+ const { displacement, isAfterDraggedBlocks, isNesting } =
+ getDragDisplacementValues( {
+ blockIndexes,
+ blockDropTargetIndex,
+ blockDropPosition,
+ clientId,
+ firstDraggedBlockIndex,
+ isDragged,
+ } );
+
+ rows.push(
+
+ { showBlock && (
+
+ ) }
+ { showNestedBlocks && (
+
+ ) }
+
+ );
+ } );
+
+ pushPlaceholderRow();
+
+ return (
+ <>
+ { rows }
{ showAppender && (
{
@@ -61,8 +61,6 @@ const expanded = ( state, action ) => {
return state;
};
-export const BLOCK_LIST_ITEM_HEIGHT = 32;
-
/** @typedef {React.ComponentType} ComponentType */
/** @typedef {React.Ref} Ref */
@@ -322,9 +320,6 @@ function ListViewComponent(
]
);
- // List View renders a fixed number of items and relies on each having a fixed item height of 36px.
- // If this value changes, we should also change the itemHeight value set in useFixedWindowList.
- // See: https://github.com/WordPress/gutenberg/pull/35230 for additional context.
const [ fixedListWindow ] = useFixedWindowList(
elementRef,
BLOCK_LIST_ITEM_HEIGHT,
diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss
index f2f7a8183c12cb..3f096ea27cfa0b 100644
--- a/packages/block-editor/src/components/list-view/style.scss
+++ b/packages/block-editor/src/components/list-view/style.scss
@@ -568,6 +568,7 @@ svg {
.block-editor-list-view-placeholder {
padding: 0;
margin: 0;
+ // One row. Spacers standing in for several rows set their height inline.
height: 32px;
}
diff --git a/packages/block-editor/src/components/list-view/utils.js b/packages/block-editor/src/components/list-view/utils.js
index 66db52a7ab06ac..d88985af2bf289 100644
--- a/packages/block-editor/src/components/list-view/utils.js
+++ b/packages/block-editor/src/components/list-view/utils.js
@@ -4,6 +4,10 @@
import { __, sprintf } from '@wordpress/i18n';
import { focus } from '@wordpress/dom';
+// Must match the row height in `style.scss`; the windowing math relies on it.
+// See: https://github.com/WordPress/gutenberg/pull/35230 for additional context.
+export const BLOCK_LIST_ITEM_HEIGHT = 32;
+
export const getBlockPositionDescription = ( position, siblingCount, level ) =>
sprintf(
/* translators: 1: The numerical position of the block. 2: The total number of blocks. 3. The level of nesting for the block. */
diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json
index 2b3d5c06ffad24..78424075d3017d 100644
--- a/tools/eslint/suppressions.json
+++ b/tools/eslint/suppressions.json
@@ -265,11 +265,6 @@
"count": 1
}
},
- "packages/block-editor/src/components/list-view/branch.js": {
- "react-hooks/refs": {
- "count": 2
- }
- },
"packages/block-editor/src/components/list-view/drop-indicator.js": {
"@wordpress/use-recommended-components": {
"count": 1