diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json
index 1667cca1371e..b9c22994df5f 100644
--- a/docs/pages/x/api/data-grid/data-grid-premium.json
+++ b/docs/pages/x/api/data-grid/data-grid-premium.json
@@ -588,7 +588,11 @@
     "rowGroupingModel": { "type": { "name": "arrayOf", "description": "Array<string>" } },
     "rowHeight": { "type": { "name": "number" }, "default": "52" },
     "rowModesModel": { "type": { "name": "object" } },
-    "rowPositionsDebounceMs": { "type": { "name": "number" }, "default": "166" },
+    "rowPositionsDebounceMs": {
+      "type": { "name": "number" },
+      "default": "166",
+      "deprecated": true
+    },
     "rowReordering": { "type": { "name": "bool" }, "default": "false" },
     "rows": {
       "type": { "name": "arrayOf", "description": "Array<object>" },
diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json
index cc714abf8fba..f0e2cc6f48c4 100644
--- a/docs/pages/x/api/data-grid/data-grid-pro.json
+++ b/docs/pages/x/api/data-grid/data-grid-pro.json
@@ -523,7 +523,11 @@
     "rowCount": { "type": { "name": "number" } },
     "rowHeight": { "type": { "name": "number" }, "default": "52" },
     "rowModesModel": { "type": { "name": "object" } },
-    "rowPositionsDebounceMs": { "type": { "name": "number" }, "default": "166" },
+    "rowPositionsDebounceMs": {
+      "type": { "name": "number" },
+      "default": "166",
+      "deprecated": true
+    },
     "rowReordering": { "type": { "name": "bool" }, "default": "false" },
     "rows": {
       "type": { "name": "arrayOf", "description": "Array<object>" },
diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json
index 3fd158be8aa0..25e56f8402bc 100644
--- a/docs/pages/x/api/data-grid/data-grid.json
+++ b/docs/pages/x/api/data-grid/data-grid.json
@@ -439,7 +439,11 @@
     "rowCount": { "type": { "name": "number" } },
     "rowHeight": { "type": { "name": "number" }, "default": "52" },
     "rowModesModel": { "type": { "name": "object" } },
-    "rowPositionsDebounceMs": { "type": { "name": "number" }, "default": "166" },
+    "rowPositionsDebounceMs": {
+      "type": { "name": "number" },
+      "default": "166",
+      "deprecated": true
+    },
     "rows": {
       "type": { "name": "arrayOf", "description": "Array<object>" },
       "default": "[]"
diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
index 89e77912b74c..86be3303b736 100644
--- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
+++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
@@ -967,6 +967,7 @@ DataGridPremiumRaw.propTypes = {
    * Setting it to a lower value could be useful when using dynamic row height,
    * but might reduce performance when displaying a large number of rows.
    * @default 166
+   * @deprecated
    */
   rowPositionsDebounceMs: PropTypes.number,
   /**
diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
index 4590c79ccfdf..8cea3a676d0d 100644
--- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
+++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
@@ -873,6 +873,7 @@ DataGridProRaw.propTypes = {
    * Setting it to a lower value could be useful when using dynamic row height,
    * but might reduce performance when displaying a large number of rows.
    * @default 166
+   * @deprecated
    */
   rowPositionsDebounceMs: PropTypes.number,
   /**
diff --git a/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts b/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts
index f618ffe20a7f..2efc3fbe9250 100644
--- a/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts
+++ b/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts
@@ -313,7 +313,13 @@ export const useGridDetailPanel = (
 
   const isFirstRender = React.useRef(true);
   if (isFirstRender.current) {
-    isFirstRender.current = false;
     updateCachesIfNeeded();
   }
+  React.useEffect(() => {
+    if (!isFirstRender.current) {
+      updateCachesIfNeeded();
+      apiRef.current.hydrateRowsMeta();
+    }
+    isFirstRender.current = false;
+  }, [apiRef, updateCachesIfNeeded]);
 };
diff --git a/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx
index 3acb545b4ed5..ba199d341d21 100644
--- a/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx
+++ b/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx
@@ -887,77 +887,6 @@ describe('<DataGridPro /> - Rows', () => {
     });
   });
 
-  describe('apiRef: setRowHeight', () => {
-    const ROW_HEIGHT = 52;
-
-    before(function beforeHook() {
-      if (isJSDOM) {
-        // Need layouting
-        this.skip();
-      }
-    });
-
-    beforeEach(() => {
-      baselineProps = {
-        rows: [
-          {
-            id: 0,
-            brand: 'Nike',
-          },
-          {
-            id: 1,
-            brand: 'Adidas',
-          },
-          {
-            id: 2,
-            brand: 'Puma',
-          },
-        ],
-        columns: [{ field: 'brand', headerName: 'Brand' }],
-      };
-    });
-
-    let apiRef: React.MutableRefObject<GridApi>;
-
-    function TestCase(props: Partial<DataGridProProps>) {
-      apiRef = useGridApiRef();
-      return (
-        <div style={{ width: 300, height: 300 }}>
-          <DataGridPro {...baselineProps} apiRef={apiRef} rowHeight={ROW_HEIGHT} {...props} />
-        </div>
-      );
-    }
-
-    it('should change row height', () => {
-      const resizedRowId = 1;
-      render(<TestCase />);
-
-      expect(getRow(1).clientHeight).to.equal(ROW_HEIGHT);
-
-      act(() => apiRef.current.unstable_setRowHeight(resizedRowId, 100));
-      expect(getRow(resizedRowId).clientHeight).to.equal(100);
-    });
-
-    it('should preserve changed row height after sorting', () => {
-      const resizedRowId = 0;
-      const getRowHeight = spy();
-      render(<TestCase getRowHeight={getRowHeight} />);
-
-      const row = getRow(resizedRowId);
-      expect(row.clientHeight).to.equal(ROW_HEIGHT);
-
-      getRowHeight.resetHistory();
-      act(() => apiRef.current.unstable_setRowHeight(resizedRowId, 100));
-      expect(row.clientHeight).to.equal(100);
-
-      // sort
-      fireEvent.click(getColumnHeaderCell(resizedRowId));
-
-      expect(row.clientHeight).to.equal(100);
-      expect(getRowHeight.neverCalledWithMatch({ id: resizedRowId })).to.equal(true);
-    });
-  });
-
   describe('prop: rowCount', () => {
     function TestCase(props: DataGridProProps) {
       return (
diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx
index 72a2bb43cb78..ccda733ed0de 100644
--- a/packages/x-data-grid/src/DataGrid/DataGrid.tsx
+++ b/packages/x-data-grid/src/DataGrid/DataGrid.tsx
@@ -739,6 +739,7 @@ DataGridRaw.propTypes = {
    * Setting it to a lower value could be useful when using dynamic row height,
    * but might reduce performance when displaying a large number of rows.
    * @default 166
+   * @deprecated
    */
   rowPositionsDebounceMs: PropTypes.number,
   /**
diff --git a/packages/x-data-grid/src/components/GridRow.tsx b/packages/x-data-grid/src/components/GridRow.tsx
index 03c8b20797fc..f326364029d1 100644
--- a/packages/x-data-grid/src/components/GridRow.tsx
+++ b/packages/x-data-grid/src/components/GridRow.tsx
@@ -6,7 +6,6 @@ import { fastMemo } from '@mui/x-internals/fastMemo';
 import { GridRowEventLookup } from '../models/events';
 import { GridRowId, GridRowModel } from '../models/gridRows';
 import { GridEditModes, GridRowModes, GridCellModes } from '../models/gridEditRowModel';
-import { useGridApiContext } from '../hooks/utils/useGridApiContext';
 import { gridClasses } from '../constants/gridClasses';
 import { composeGridClasses } from '../utils/composeGridClasses';
 import { useGridRootProps } from '../hooks/utils/useGridRootProps';
@@ -29,6 +28,7 @@ import { PinnedPosition, gridPinnedColumnPositionLookup } from './cell/GridCell'
 import { GridScrollbarFillerCell as ScrollbarFiller } from './GridScrollbarFillerCell';
 import { getPinnedCellOffset } from '../internals/utils/getPinnedCellOffset';
 import { useGridConfiguration } from '../hooks/utils/useGridConfiguration';
+import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext';
 
 export interface GridRowProps extends React.HTMLAttributes<HTMLDivElement> {
   row: GridRowModel;
@@ -111,7 +111,7 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(function GridRow(
     onMouseOver,
     ...other
   } = props;
-  const apiRef = useGridApiContext();
+  const apiRef = useGridPrivateApiContext();
   const configuration = useGridConfiguration();
   const ref = React.useRef<HTMLDivElement>(null);
   const rootProps = useGridRootProps();
@@ -153,37 +153,19 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(function GridRow(
 
   React.useLayoutEffect(() => {
     if (currentPage.range) {
-      // The index prop is relative to the rows from all pages. As example, the index prop of the
-      // first row is 5 if `paginationModel.pageSize=5` and `paginationModel.page=1`. However, the index used by the virtualization
-      // doesn't care about pagination and considers the rows from the current page only, so the
-      // first row always has index=0. We need to subtract the index of the first row to make it
-      // compatible with the index used by the virtualization.
       const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(rowId);
-      // pinned rows are not part of the visible rows
-      if (rowIndex != null) {
+      // Pinned rows are not part of the visible rows
+      if (rowIndex !== undefined) {
         apiRef.current.unstable_setLastMeasuredRowIndex(rowIndex);
       }
     }
 
-    const rootElement = ref.current;
-    const hasFixedHeight = rowHeight !== 'auto';
-    if (!rootElement || hasFixedHeight || typeof ResizeObserver === 'undefined') {
-      return undefined;
+    if (ref.current && rowHeight === 'auto') {
+      return apiRef.current.observeRowHeight(ref.current, rowId);
     }
 
-    const resizeObserver = new ResizeObserver((entries) => {
-      const [entry] = entries;
-      const height =
-        entry.borderBoxSize && entry.borderBoxSize.length > 0
-          ? entry.borderBoxSize[0].blockSize
-          : entry.contentRect.height;
-      apiRef.current.unstable_storeRowHeightMeasurement(rowId, height);
-    });
-
-    resizeObserver.observe(rootElement);
-
-    return () => resizeObserver.disconnect();
-  }, [apiRef, currentPage.range, index, rowHeight, rowId]);
+    return undefined;
+  }, [apiRef, currentPage.range, rowHeight, rowId]);
 
   const publish = React.useCallback(
     (
@@ -254,22 +236,12 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(function GridRow(
 
   const rowReordering = (rootProps as any).rowReordering as boolean;
 
-  const sizes = useGridSelector(
+  const heightEntry = useGridSelector(
     apiRef,
-    () => ({ ...apiRef.current.unstable_getRowInternalSizes(rowId) }),
+    () => ({ ...apiRef.current.getRowHeightEntry(rowId) }),
     objectShallowCompare,
   );
 
-  let minHeight = rowHeight;
-  if (minHeight === 'auto' && sizes) {
-    const numberOfBaseSizes = 1;
-    const maximumSize = sizes.baseCenter ?? 0;
-
-    if (maximumSize > 0 && numberOfBaseSizes > 1) {
-      minHeight = maximumSize;
-    }
-  }
-
   const style = React.useMemo(() => {
     if (isNotVisible) {
       return {
@@ -282,28 +254,28 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(function GridRow(
     const rowStyle = {
       ...styleProp,
       maxHeight: rowHeight === 'auto' ? 'none' : rowHeight, // max-height doesn't support "auto"
-      minHeight,
+      minHeight: rowHeight,
       '--height': typeof rowHeight === 'number' ? `${rowHeight}px` : rowHeight,
     };
 
-    if (sizes?.spacingTop) {
+    if (heightEntry.spacingTop) {
       const property = rootProps.rowSpacingType === 'border' ? 'borderTopWidth' : 'marginTop';
-      rowStyle[property] = sizes.spacingTop;
+      rowStyle[property] = heightEntry.spacingTop;
     }
 
-    if (sizes?.spacingBottom) {
+    if (heightEntry.spacingBottom) {
       const property = rootProps.rowSpacingType === 'border' ? 'borderBottomWidth' : 'marginBottom';
       let propertyValue = rowStyle[property];
       // avoid overriding existing value
       if (typeof propertyValue !== 'number') {
         propertyValue = parseInt(propertyValue || '0', 10);
       }
-      propertyValue += sizes.spacingBottom;
+      propertyValue += heightEntry.spacingBottom;
       rowStyle[property] = propertyValue;
     }
 
     return rowStyle;
-  }, [isNotVisible, rowHeight, styleProp, minHeight, sizes, rootProps.rowSpacingType]);
+  }, [isNotVisible, rowHeight, styleProp, heightEntry, rootProps.rowSpacingType]);
 
   const rowClassNames = apiRef.current.unstable_applyPipeProcessors('rowClassName', [], rowId);
   const ariaAttributes = rowNode ? getRowAriaAttributes(rowNode, index) : undefined;
diff --git a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts
index 5e065f1a572a..7cd516a68c06 100644
--- a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts
+++ b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts
@@ -20,6 +20,7 @@ import {
 import { GridRowEntry, GridRowId } from '../../../models/gridRows';
 import { GridHydrateRowsValue } from '../../features/rows/gridRowsInterfaces';
 import { GridPreferencePanelsValue } from '../../features/preferencesPanel';
+import { HeightEntry } from '../../features/rows/gridRowsMetaInterfaces';
 
 export type GridPipeProcessorGroup = keyof GridPipeProcessingLookup;
 
@@ -41,7 +42,7 @@ export interface GridPipeProcessingLookup {
     value: GridRestoreStatePreProcessingValue;
     context: GridRestoreStatePreProcessingContext<GridInitialStateCommunity>;
   };
-  rowHeight: { value: Record<string, number>; context: GridRowEntry };
+  rowHeight: { value: HeightEntry; context: GridRowEntry };
   scrollToIndexes: {
     value: Partial<GridScrollParams>;
     context: Partial<GridCellIndexCoordinates>;
diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowsMetaInterfaces.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowsMetaInterfaces.ts
new file mode 100644
index 000000000000..c9fee83fd2ad
--- /dev/null
+++ b/packages/x-data-grid/src/hooks/features/rows/gridRowsMetaInterfaces.ts
@@ -0,0 +1,20 @@
+import { GridRowId } from '../../../models/gridRows';
+
+export type HeightEntry = {
+  content: number;
+  spacingTop: number;
+  spacingBottom: number;
+  detail: number;
+
+  autoHeight: boolean;
+  needsFirstMeasurement: boolean;
+};
+
+export type HeightCache = Map<GridRowId, HeightEntry>;
+
+export interface GridRowsMetaInternalCache {
+  /**
+   * Map of height cache entries.
+   */
+  heights: HeightCache;
+}
diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts
index e7b217aaa9a3..5630a73d551d 100644
--- a/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts
+++ b/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts
@@ -464,3 +464,8 @@ export const rowHeightWarning = [
   `MUI X: The \`rowHeight\` prop should be a number greater than 0.`,
   `The default value will be used instead.`,
 ].join('\n');
+
+export const getRowHeightWarning = [
+  `MUI X: The \`getRowHeight\` prop should return a number greater than 0 or 'auto'.`,
+  `The default value will be used instead.`,
+].join('\n');
diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts
index bb49e04a9ae6..501b8690a710 100644
--- a/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts
+++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts
@@ -1,11 +1,13 @@
 import * as React from 'react';
-import { unstable_debounce as debounce } from '@mui/utils';
+import useLazyRef from '@mui/utils/useLazyRef';
+import { ResizeObserver } from '../../../utils/ResizeObserver';
 import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity';
 import { GridRowsMetaApi, GridRowsMetaPrivateApi } from '../../../models/api/gridRowsMetaApi';
 import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
 import { useGridVisibleRows } from '../../utils/useGridVisibleRows';
+import { eslintUseValue } from '../../../utils/utils';
 import { useGridApiMethod } from '../../utils/useGridApiMethod';
-import { GridRowEntry, GridRowId } from '../../../models/gridRows';
+import { GridRowEntry } from '../../../models/gridRows';
 import { useGridSelector } from '../../utils/useGridSelector';
 import { gridDensityFactorSelector } from '../density/densitySelector';
 import { gridFilterModelSelector } from '../filter/gridFilterSelector';
@@ -15,22 +17,24 @@ import { GridStateInitializer } from '../../utils/useGridInitializeState';
 import { useGridRegisterPipeApplier } from '../../core/pipeProcessing';
 import { gridPinnedRowsSelector } from './gridRowsSelector';
 import { gridDimensionsSelector } from '../dimensions/gridDimensionsSelectors';
-import { getValidRowHeight } from './gridRowsUtils';
+import { getValidRowHeight, getRowHeightWarning } from './gridRowsUtils';
+import type { HeightEntry } from './gridRowsMetaInterfaces';
 
-// TODO: I think the row heights can now be encoded as a single `size` instead of `sizes.baseXxxx`
+/* eslint-disable no-underscore-dangle */
 
-export const rowsMetaStateInitializer: GridStateInitializer = (state) => ({
-  ...state,
-  rowsMeta: {
-    currentPageTotalHeight: 0,
-    positions: [],
-  },
-});
+export const rowsMetaStateInitializer: GridStateInitializer = (state, props, apiRef) => {
+  apiRef.current.caches.rowsMeta = {
+    heights: new Map(),
+  };
 
-const getRowHeightWarning = [
-  `MUI X: The \`getRowHeight\` prop should return a number greater than 0 or 'auto'.`,
-  `The default value will be used instead.`,
-].join('\n');
+  return {
+    ...state,
+    rowsMeta: {
+      currentPageTotalHeight: 0,
+      positions: [],
+    },
+  };
+};
 
 /**
  * @requires useGridPageSize (method)
@@ -50,18 +54,12 @@ export const useGridRowsMeta = (
   >,
 ): void => {
   const { getRowHeight: getRowHeightProp, getRowSpacing, getEstimatedRowHeight } = props;
-  const rowsHeightLookup = React.useRef<{
-    [key: GridRowId]: {
-      isResized: boolean;
-      sizes: Record<string, number>;
-      autoHeight: boolean; // Determines if the row has dynamic height
-      needsFirstMeasurement: boolean; // Determines if the row was never measured. If true, use the estimated height as row height.
-    };
-  }>(Object.create(null));
-
-  // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
+  const heightCache = apiRef.current.caches.rowsMeta.heights;
+
   const lastMeasuredRowIndex = React.useRef(-1);
   const hasRowWithAutoHeight = React.useRef(false);
+  const isHeightMetaValid = React.useRef(false);
+
   const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector);
   const filterModel = useGridSelector(apiRef, gridFilterModelSelector);
   const paginationState = useGridSelector(apiRef, gridPaginationSelector);
@@ -73,55 +71,57 @@ export const useGridRowsMeta = (
     () => gridDimensionsSelector(apiRef.current.state).rowHeight,
   );
 
-  const hydrateRowsMeta = React.useCallback(() => {
-    hasRowWithAutoHeight.current = false;
+  const getRowHeightEntry: GridRowsMetaPrivateApi['getRowHeightEntry'] = (rowId) => {
+    let entry = heightCache.get(rowId);
+    if (entry === undefined) {
+      entry = {
+        content: rowHeight,
+        spacingTop: 0,
+        spacingBottom: 0,
+        detail: 0,
+        autoHeight: false,
+        needsFirstMeasurement: true,
+      };
+      heightCache.set(rowId, entry);
+    }
+    return entry;
+  };
 
-    const calculateRowProcessedSizes = (row: GridRowEntry) => {
-      if (!rowsHeightLookup.current[row.id]) {
-        rowsHeightLookup.current[row.id] = {
-          sizes: { baseCenter: rowHeight },
-          isResized: false,
-          autoHeight: false,
-          needsFirstMeasurement: true, // Assume all rows will need to be measured by default
-        };
-      }
+  const processHeightEntry = React.useCallback(
+    (row: GridRowEntry) => {
+      // HACK: rowHeight trails behind the most up-to-date value just enough to
+      // mess the initial rowsMeta hydration :/
+      const baseRowHeight = gridDimensionsSelector(apiRef.current.state).rowHeight;
+      eslintUseValue(rowHeight);
 
-      const { isResized, needsFirstMeasurement, sizes } = rowsHeightLookup.current[row.id];
-      let baseRowHeight = typeof rowHeight === 'number' && rowHeight > 0 ? rowHeight : 52;
-      const existingBaseRowHeight = sizes.baseCenter;
+      const entry = apiRef.current.getRowHeightEntry(row.id);
 
-      if (isResized) {
-        // Do not recalculate resized row height and use the value from the lookup
-        baseRowHeight = existingBaseRowHeight;
-      } else if (getRowHeightProp) {
+      if (!getRowHeightProp) {
+        entry.content = baseRowHeight;
+        entry.needsFirstMeasurement = false;
+      } else {
         const rowHeightFromUser = getRowHeightProp({ ...row, densityFactor });
 
         if (rowHeightFromUser === 'auto') {
-          if (needsFirstMeasurement) {
+          if (entry.needsFirstMeasurement) {
             const estimatedRowHeight = getEstimatedRowHeight
               ? getEstimatedRowHeight({ ...row, densityFactor })
-              : rowHeight;
+              : baseRowHeight;
 
             // If the row was not measured yet use the estimated row height
-            baseRowHeight = estimatedRowHeight ?? rowHeight;
-          } else {
-            baseRowHeight = existingBaseRowHeight;
+            entry.content = estimatedRowHeight ?? baseRowHeight;
           }
 
           hasRowWithAutoHeight.current = true;
-          rowsHeightLookup.current[row.id].autoHeight = true;
+          entry.autoHeight = true;
         } else {
           // Default back to base rowHeight if getRowHeight returns invalid value.
-          baseRowHeight = getValidRowHeight(rowHeightFromUser, rowHeight, getRowHeightWarning);
-          rowsHeightLookup.current[row.id].needsFirstMeasurement = false;
-          rowsHeightLookup.current[row.id].autoHeight = false;
+          entry.content = getValidRowHeight(rowHeightFromUser, baseRowHeight, getRowHeightWarning);
+          entry.needsFirstMeasurement = false;
+          entry.autoHeight = false;
         }
-      } else {
-        rowsHeightLookup.current[row.id].needsFirstMeasurement = false;
       }
 
-      const initialHeights = { baseCenter: baseRowHeight } as Record<string, number>;
-
       if (getRowSpacing) {
         const indexRelativeToCurrentPage = apiRef.current.getRowIndexRelativeToVisibleRows(row.id);
 
@@ -132,46 +132,48 @@ export const useGridRowsMeta = (
           indexRelativeToCurrentPage,
         });
 
-        initialHeights.spacingTop = spacing.top ?? 0;
-        initialHeights.spacingBottom = spacing.bottom ?? 0;
+        entry.spacingTop = spacing.top ?? 0;
+        entry.spacingBottom = spacing.bottom ?? 0;
+      } else {
+        entry.spacingTop = 0;
+        entry.spacingBottom = 0;
       }
 
-      const processedSizes = apiRef.current.unstable_applyPipeProcessors(
-        'rowHeight',
-        initialHeights,
-        row,
-      ) as Record<string, number>;
+      apiRef.current.unstable_applyPipeProcessors('rowHeight', entry, row) as HeightEntry;
 
-      rowsHeightLookup.current[row.id].sizes = processedSizes;
+      return entry;
+    },
+    [
+      apiRef,
+      currentPage.rows.length,
+      getRowHeightProp,
+      getEstimatedRowHeight,
+      rowHeight,
+      getRowSpacing,
+      densityFactor,
+    ],
+  );
 
-      return processedSizes;
-    };
+  const hydrateRowsMeta = React.useCallback(() => {
+    hasRowWithAutoHeight.current = false;
+
+    pinnedRows.top.forEach(processHeightEntry);
+    pinnedRows.bottom.forEach(processHeightEntry);
 
     const positions: number[] = [];
     const currentPageTotalHeight = currentPage.rows.reduce((acc, row) => {
       positions.push(acc);
 
-      let otherSizes = 0;
+      const entry = processHeightEntry(row);
+      const total = entry.content + entry.spacingTop + entry.spacingBottom + entry.detail;
 
-      const processedSizes = calculateRowProcessedSizes(row);
-      /* eslint-disable-next-line guard-for-in */
-      for (const key in processedSizes) {
-        const value = processedSizes[key];
-        if (key !== 'baseCenter') {
-          otherSizes += value;
-        }
-      }
-
-      return acc + processedSizes.baseCenter + otherSizes;
+      return acc + total;
     }, 0);
 
-    pinnedRows?.top?.forEach((row) => {
-      calculateRowProcessedSizes(row);
-    });
-
-    pinnedRows?.bottom?.forEach((row) => {
-      calculateRowProcessedSizes(row);
-    });
+    if (!hasRowWithAutoHeight.current) {
+      // No row has height=auto, so all rows are already measured
+      lastMeasuredRowIndex.current = Infinity;
+    }
 
     apiRef.current.setState((state) => {
       return {
@@ -183,113 +185,93 @@ export const useGridRowsMeta = (
       };
     });
 
-    if (!hasRowWithAutoHeight.current) {
-      // No row has height=auto, so all rows are already measured
-      lastMeasuredRowIndex.current = Infinity;
-    }
+    isHeightMetaValid.current = true;
+  }, [apiRef, pinnedRows, currentPage.rows, processHeightEntry]);
 
-    apiRef.current.forceUpdate();
-  }, [
-    apiRef,
-    currentPage.rows,
-    rowHeight,
-    getRowHeightProp,
-    getRowSpacing,
-    getEstimatedRowHeight,
-    pinnedRows,
-    densityFactor,
-  ]);
-
-  const getRowHeight = React.useCallback<GridRowsMetaApi['unstable_getRowHeight']>(
-    (rowId) => {
-      const height = rowsHeightLookup.current[rowId];
-      return height ? height.sizes.baseCenter : rowHeight;
-    },
-    [rowHeight],
-  );
-
-  const getRowInternalSizes = (rowId: GridRowId): Record<string, number> | undefined =>
-    rowsHeightLookup.current[rowId]?.sizes;
-
-  const setRowHeight = React.useCallback<GridRowsMetaApi['unstable_setRowHeight']>(
-    (id: GridRowId, height: number) => {
-      rowsHeightLookup.current[id].sizes.baseCenter = height;
-      rowsHeightLookup.current[id].isResized = true;
-      rowsHeightLookup.current[id].needsFirstMeasurement = false;
-      hydrateRowsMeta();
-    },
-    [hydrateRowsMeta],
-  );
-
-  const debouncedHydrateRowsMeta = React.useMemo(
-    () => debounce(hydrateRowsMeta, props.rowPositionsDebounceMs),
-    [hydrateRowsMeta, props.rowPositionsDebounceMs],
-  );
+  const getRowHeight: GridRowsMetaApi['unstable_getRowHeight'] = (rowId) => {
+    return heightCache.get(rowId)?.content ?? rowHeight;
+  };
 
-  const storeMeasuredRowHeight = React.useCallback<
-    GridRowsMetaApi['unstable_storeRowHeightMeasurement']
-  >(
-    (id, height) => {
-      if (!rowsHeightLookup.current[id] || !rowsHeightLookup.current[id].autoHeight) {
-        return;
-      }
+  const storeRowHeightMeasurement: GridRowsMetaApi['unstable_storeRowHeightMeasurement'] = (
+    id,
+    height,
+  ) => {
+    const entry = apiRef.current.getRowHeightEntry(id);
 
-      // Only trigger hydration if the value is different, otherwise we trigger a loop
-      const needsHydration = rowsHeightLookup.current[id].sizes.baseCenter !== height;
+    const didChange = entry.content !== height;
 
-      rowsHeightLookup.current[id].needsFirstMeasurement = false;
-      rowsHeightLookup.current[id].sizes.baseCenter = height;
+    entry.needsFirstMeasurement = false;
+    entry.content = height;
 
-      if (needsHydration) {
-        debouncedHydrateRowsMeta();
-      }
-    },
-    [debouncedHydrateRowsMeta],
-  );
+    isHeightMetaValid.current &&= !didChange;
+  };
 
-  const rowHasAutoHeight = React.useCallback<GridRowsMetaPrivateApi['rowHasAutoHeight']>((id) => {
-    return rowsHeightLookup.current[id]?.autoHeight || false;
-  }, []);
+  const rowHasAutoHeight: GridRowsMetaPrivateApi['rowHasAutoHeight'] = (id) => {
+    return heightCache.get(id)?.autoHeight ?? false;
+  };
 
-  const getLastMeasuredRowIndex = React.useCallback<
-    GridRowsMetaPrivateApi['getLastMeasuredRowIndex']
-  >(() => {
+  const getLastMeasuredRowIndex: GridRowsMetaPrivateApi['getLastMeasuredRowIndex'] = () => {
     return lastMeasuredRowIndex.current;
-  }, []);
+  };
 
-  const setLastMeasuredRowIndex = React.useCallback<
-    GridRowsMetaApi['unstable_setLastMeasuredRowIndex']
-  >((index) => {
+  const setLastMeasuredRowIndex: GridRowsMetaApi['unstable_setLastMeasuredRowIndex'] = (index) => {
     if (hasRowWithAutoHeight.current && index > lastMeasuredRowIndex.current) {
       lastMeasuredRowIndex.current = index;
     }
-  }, []);
+  };
 
-  const resetRowHeights = React.useCallback(() => {
-    rowsHeightLookup.current = {};
+  const resetRowHeights: GridRowsMetaApi['resetRowHeights'] = () => {
+    heightCache.clear();
     hydrateRowsMeta();
-  }, [hydrateRowsMeta]);
+  };
+
+  const resizeObserver = useLazyRef(
+    () =>
+      new ResizeObserver((entries) => {
+        for (let i = 0; i < entries.length; i += 1) {
+          const entry = entries[i];
+          const height =
+            entry.borderBoxSize && entry.borderBoxSize.length > 0
+              ? entry.borderBoxSize[0].blockSize
+              : entry.contentRect.height;
+          const rowId = (entry.target as any).__mui_id;
+          apiRef.current.unstable_storeRowHeightMeasurement(rowId, height);
+        }
+        if (!isHeightMetaValid.current) {
+          apiRef.current.requestPipeProcessorsApplication('rowHeight');
+        }
+      }),
+  ).current;
+
+  const observeRowHeight: GridRowsMetaPrivateApi['observeRowHeight'] = (element, rowId) => {
+    (element as any).__mui_id = rowId;
+
+    resizeObserver.observe(element);
+
+    return () => resizeObserver.unobserve(element);
+  };
+
+  useGridRegisterPipeApplier(apiRef, 'rowHeight', hydrateRowsMeta);
 
   // The effect is used to build the rows meta data - currentPageTotalHeight and positions.
   // Because of variable row height this is needed for the virtualization
   React.useEffect(() => {
     hydrateRowsMeta();
-  }, [rowHeight, filterModel, paginationState, sortModel, hydrateRowsMeta]);
-
-  useGridRegisterPipeApplier(apiRef, 'rowHeight', hydrateRowsMeta);
+  }, [filterModel, paginationState, sortModel, hydrateRowsMeta]);
 
   const rowsMetaApi: GridRowsMetaApi = {
-    unstable_setLastMeasuredRowIndex: setLastMeasuredRowIndex,
     unstable_getRowHeight: getRowHeight,
-    unstable_getRowInternalSizes: getRowInternalSizes,
-    unstable_setRowHeight: setRowHeight,
-    unstable_storeRowHeightMeasurement: storeMeasuredRowHeight,
+    unstable_setLastMeasuredRowIndex: setLastMeasuredRowIndex,
+    unstable_storeRowHeightMeasurement: storeRowHeightMeasurement,
     resetRowHeights,
   };
 
   const rowsMetaPrivateApi: GridRowsMetaPrivateApi = {
-    getLastMeasuredRowIndex,
+    hydrateRowsMeta,
+    observeRowHeight,
     rowHasAutoHeight,
+    getRowHeightEntry,
+    getLastMeasuredRowIndex,
   };
 
   useGridApiMethod(apiRef, rowsMetaApi, 'public');
diff --git a/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts b/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts
index fb7935e9d913..bbaaa2b3eb6c 100644
--- a/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts
+++ b/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts
@@ -1,4 +1,5 @@
 import * as React from 'react';
+import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
 import { GridPrivateApiCommon } from '../../models/api/gridApiCommon';
 
 type GetPublicApiType<PrivateApi> = PrivateApi extends { getPublicApi: () => infer PublicApi }
@@ -14,7 +15,7 @@ export function useGridApiMethod<
 >(privateApiRef: React.MutableRefObject<PrivateApi>, apiMethods: T, visibility: V) {
   const isFirstRender = React.useRef(true);
 
-  React.useEffect(() => {
+  useEnhancedEffect(() => {
     isFirstRender.current = false;
     privateApiRef.current.register(visibility, apiMethods);
   }, [privateApiRef, visibility, apiMethods]);
diff --git a/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts b/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts
index 77e744b599aa..9dba011f4175 100644
--- a/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts
+++ b/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts
@@ -1,4 +1,5 @@
 import { GridRowId } from '../gridRows';
+import { HeightEntry } from '../../hooks/features/rows/gridRowsMetaInterfaces';
 
 /**
  * The Row Meta API interface that is available in the grid `apiRef`.
@@ -11,20 +12,6 @@ export interface GridRowsMetaApi {
    * @ignore - do not document.
    */
   unstable_getRowHeight: (id: GridRowId) => number;
-  /**
-   * Gets all sizes that compose the total height that the given row takes.
-   * @param {GridRowId} id The id of the row.
-   * @returns {Record<string, number>} The object containing the sizes.
-   * @ignore - do not document.
-   */
-  unstable_getRowInternalSizes: (id: GridRowId) => Record<string, number> | undefined;
-  /**
-   * Updates the base height of a row.
-   * @param {GridRowId} id The id of the row.
-   * @param {number} height The new height.
-   * @ignore - do not document.
-   */
-  unstable_setRowHeight: (id: GridRowId, height: number) => void;
   /**
    * Stores the row height measurement and triggers an hydration, if needed.
    * @param {GridRowId} id The id of the row.
@@ -46,6 +33,14 @@ export interface GridRowsMetaApi {
 }
 
 export interface GridRowsMetaPrivateApi {
+  hydrateRowsMeta: () => void;
+  /**
+   * Observe row for 'auto' height changes.
+   * @param {Element} element The row element to observe.
+   * @param {GridRowId} rowId The id of the row.
+   * @returns {ReturnType<React.EffectCallback>} A dispose callback
+   */
+  observeRowHeight: (element: Element, rowId: GridRowId) => ReturnType<React.EffectCallback>;
   /**
    * Determines if the height of a row is "auto".
    * @param {GridRowId} id The id of the row.
@@ -58,4 +53,10 @@ export interface GridRowsMetaPrivateApi {
    * @returns {number} The index of the last measured row.
    */
   getLastMeasuredRowIndex: () => number;
+  /**
+   * Get the height entry from the cache or create one.
+   * @param {GridRowId} id The id of the row.
+   * @returns {HeightEntry} The height cache entry
+   */
+  getRowHeightEntry: (id: GridRowId) => HeightEntry;
 }
diff --git a/packages/x-data-grid/src/models/gridApiCaches.ts b/packages/x-data-grid/src/models/gridApiCaches.ts
index aa2e8932b91d..5b3652b69224 100644
--- a/packages/x-data-grid/src/models/gridApiCaches.ts
+++ b/packages/x-data-grid/src/models/gridApiCaches.ts
@@ -1,5 +1,7 @@
 import { GridRowsInternalCache } from '../hooks/features/rows/gridRowsInterfaces';
+import { GridRowsMetaInternalCache } from '../hooks/features/rows/gridRowsMetaInterfaces';
 
 export interface GridApiCaches {
   rows: GridRowsInternalCache;
+  rowsMeta: GridRowsMetaInternalCache;
 }
diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts
index 866b882977f6..e831f19f6eeb 100644
--- a/packages/x-data-grid/src/models/props/DataGridProps.ts
+++ b/packages/x-data-grid/src/models/props/DataGridProps.ts
@@ -377,8 +377,9 @@ export interface DataGridPropsWithDefaultValues<R extends GridValidRowModel = an
    * Setting it to a lower value could be useful when using dynamic row height,
    * but might reduce performance when displaying a large number of rows.
    * @default 166
+   * @deprecated
    */
-  rowPositionsDebounceMs: number;
+  rowPositionsDebounceMs: number /* TODO(v8): remove this property */;
   /**
    * If `true`, columns are autosized after the datagrid is mounted.
    * @default false
diff --git a/packages/x-data-grid/src/utils/ResizeObserver.ts b/packages/x-data-grid/src/utils/ResizeObserver.ts
new file mode 100644
index 000000000000..54ccd6515730
--- /dev/null
+++ b/packages/x-data-grid/src/utils/ResizeObserver.ts
@@ -0,0 +1,14 @@
+/* eslint-disable */
+
+/**
+ * HACK: Minimal shim to get jsdom to work.
+ */
+export const ResizeObserver = (
+  typeof globalThis.ResizeObserver !== 'undefined'
+    ? globalThis.ResizeObserver
+    : class ResizeObserver {
+        observe() {}
+        unobserve() {}
+        disconnect() {}
+      }
+) as typeof globalThis.ResizeObserver;