perf(dashboard): decrease number of rerenders of FiltersBadge (#16545)

* perf(dashboard): decrease rerenders in FiltersBadge

* implement caching of dashboard filter indicators

* Implement caching for native filter indicators
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
index b9c2b5f..317666b 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
@@ -57,6 +57,8 @@
   );
 };
 
+const indicatorsInitialState: Indicator[] = [];
+
 export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
   const dispatch = useDispatch();
   const datasources = useSelector<RootState, any>(state => state.datasources);
@@ -77,9 +79,11 @@
     state => state.dataMask,
   );
 
-  const [nativeIndicators, setNativeIndicators] = useState<Indicator[]>([]);
+  const [nativeIndicators, setNativeIndicators] = useState<Indicator[]>(
+    indicatorsInitialState,
+  );
   const [dashboardIndicators, setDashboardIndicators] = useState<Indicator[]>(
-    [],
+    indicatorsInitialState,
   );
 
   const onHighlightFilterSource = useCallback(
@@ -90,46 +94,79 @@
   );
 
   const chart = charts[chartId];
-  const prevChartStatus = usePrevious(chart?.chartStatus);
+  const prevChart = usePrevious(chart);
+  const prevChartStatus = prevChart?.chartStatus;
+  const prevDashboardFilters = usePrevious(dashboardFilters);
+  const prevDatasources = usePrevious(datasources);
+  const showIndicators =
+    chart?.chartStatus && ['rendered', 'success'].includes(chart.chartStatus);
 
-  const showIndicators = useCallback(
-    () =>
-      chart?.chartStatus && ['rendered', 'success'].includes(chart.chartStatus),
-    [chart.chartStatus],
-  );
   useEffect(() => {
-    if (!showIndicators) {
-      setDashboardIndicators([]);
-    }
-    if (prevChartStatus !== 'success') {
-      setDashboardIndicators(
-        selectIndicatorsForChart(chartId, dashboardFilters, datasources, chart),
-      );
+    if (!showIndicators && dashboardIndicators.length > 0) {
+      setDashboardIndicators(indicatorsInitialState);
+    } else if (prevChartStatus !== 'success') {
+      if (
+        chart?.queriesResponse?.[0]?.rejected_filters !==
+          prevChart?.queriesResponse?.[0]?.rejected_filters ||
+        chart?.queriesResponse?.[0]?.applied_filters !==
+          prevChart?.queriesResponse?.[0]?.applied_filters ||
+        dashboardFilters !== prevDashboardFilters ||
+        datasources !== prevDatasources
+      ) {
+        setDashboardIndicators(
+          selectIndicatorsForChart(
+            chartId,
+            dashboardFilters,
+            datasources,
+            chart,
+          ),
+        );
+      }
     }
   }, [
     chart,
     chartId,
     dashboardFilters,
+    dashboardIndicators.length,
     datasources,
+    prevChart?.queriesResponse,
     prevChartStatus,
+    prevDashboardFilters,
+    prevDatasources,
     showIndicators,
   ]);
 
+  const prevNativeFilters = usePrevious(nativeFilters);
+  const prevDashboardLayout = usePrevious(present);
+  const prevDataMask = usePrevious(dataMask);
+  const prevChartConfig = usePrevious(
+    dashboardInfo.metadata?.chart_configuration,
+  );
   useEffect(() => {
-    if (!showIndicators) {
-      setNativeIndicators([]);
-    }
-    if (prevChartStatus !== 'success') {
-      setNativeIndicators(
-        selectNativeIndicatorsForChart(
-          nativeFilters,
-          dataMask,
-          chartId,
-          chart,
-          present,
-          dashboardInfo.metadata?.chart_configuration,
-        ),
-      );
+    if (!showIndicators && nativeIndicators.length > 0) {
+      setNativeIndicators(indicatorsInitialState);
+    } else if (prevChartStatus !== 'success') {
+      if (
+        chart?.queriesResponse?.[0]?.rejected_filters !==
+          prevChart?.queriesResponse?.[0]?.rejected_filters ||
+        chart?.queriesResponse?.[0]?.applied_filters !==
+          prevChart?.queriesResponse?.[0]?.applied_filters ||
+        nativeFilters !== prevNativeFilters ||
+        present !== prevDashboardLayout ||
+        dataMask !== prevDataMask ||
+        prevChartConfig !== dashboardInfo.metadata?.chart_configuration
+      ) {
+        setNativeIndicators(
+          selectNativeIndicatorsForChart(
+            nativeFilters,
+            dataMask,
+            chartId,
+            chart,
+            present,
+            dashboardInfo.metadata?.chart_configuration,
+          ),
+        );
+      }
     }
   }, [
     chart,
@@ -137,8 +174,14 @@
     dashboardInfo.metadata?.chart_configuration,
     dataMask,
     nativeFilters,
+    nativeIndicators.length,
     present,
+    prevChart?.queriesResponse,
+    prevChartConfig,
     prevChartStatus,
+    prevDashboardLayout,
+    prevDataMask,
+    prevNativeFilters,
     showIndicators,
   ]);
 
@@ -155,17 +198,33 @@
     [dashboardIndicators, nativeIndicators],
   );
 
-  const appliedCrossFilterIndicators = indicators.filter(
-    indicator => indicator.status === IndicatorStatus.CrossFilterApplied,
+  const appliedCrossFilterIndicators = useMemo(
+    () =>
+      indicators.filter(
+        indicator => indicator.status === IndicatorStatus.CrossFilterApplied,
+      ),
+    [indicators],
   );
-  const appliedIndicators = indicators.filter(
-    indicator => indicator.status === IndicatorStatus.Applied,
+  const appliedIndicators = useMemo(
+    () =>
+      indicators.filter(
+        indicator => indicator.status === IndicatorStatus.Applied,
+      ),
+    [indicators],
   );
-  const unsetIndicators = indicators.filter(
-    indicator => indicator.status === IndicatorStatus.Unset,
+  const unsetIndicators = useMemo(
+    () =>
+      indicators.filter(
+        indicator => indicator.status === IndicatorStatus.Unset,
+      ),
+    [indicators],
   );
-  const incompatibleIndicators = indicators.filter(
-    indicator => indicator.status === IndicatorStatus.Incompatible,
+  const incompatibleIndicators = useMemo(
+    () =>
+      indicators.filter(
+        indicator => indicator.status === IndicatorStatus.Incompatible,
+      ),
+    [indicators],
   );
 
   if (
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
index 34bdfce..a6aec8e 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
@@ -16,16 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { NO_TIME_RANGE, TIME_FILTER_MAP } from 'src/explore/constants';
-import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
-import { ChartConfiguration, Filters } from 'src/dashboard/reducers/types';
-import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
 import {
   ensureIsArray,
   FeatureFlag,
   FilterState,
   isFeatureEnabled,
 } from '@superset-ui/core';
+import { NO_TIME_RANGE, TIME_FILTER_MAP } from 'src/explore/constants';
+import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
+import { ChartConfiguration, Filters } from 'src/dashboard/reducers/types';
+import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
+import { areObjectsEqual } from 'src/reduxUtils';
 import { Layout } from '../../types';
 import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
 
@@ -154,6 +155,8 @@
   path?: string[];
 };
 
+const cachedIndicatorsForChart = {};
+const cachedDashboardFilterDataForChart = {};
 // inspects redux state to find what the filter indicators should be shown for a given chart
 export const selectIndicatorsForChart = (
   chartId: number,
@@ -165,37 +168,75 @@
   // so grab the columns from the applied/rejected filters
   const appliedColumns = getAppliedColumns(chart);
   const rejectedColumns = getRejectedColumns(chart);
+  const matchingFilters = Object.values(filters).filter(
+    filter => filter.chartId !== chartId,
+  );
+  const matchingDatasources = Object.entries(datasources)
+    .filter(([key]) =>
+      matchingFilters.find(filter => filter.datasourceId === key),
+    )
+    .map(([, datasource]) => datasource);
 
-  const indicators = Object.values(filters)
-    .filter(filter => filter.chartId !== chartId)
-    .reduce(
-      (acc, filter) =>
-        acc.concat(
-          selectIndicatorsForChartFromFilter(
-            chartId,
-            filter,
-            datasources[filter.datasourceId] || {},
-            appliedColumns,
-            rejectedColumns,
-          ),
+  const cachedFilterData = cachedDashboardFilterDataForChart[chartId];
+  if (
+    cachedIndicatorsForChart[chartId] &&
+    areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
+    areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
+    areObjectsEqual(cachedFilterData?.matchingFilters, matchingFilters) &&
+    areObjectsEqual(cachedFilterData?.matchingDatasources, matchingDatasources)
+  ) {
+    return cachedIndicatorsForChart[chartId];
+  }
+  const indicators = matchingFilters.reduce(
+    (acc, filter) =>
+      acc.concat(
+        selectIndicatorsForChartFromFilter(
+          chartId,
+          filter,
+          datasources[filter.datasourceId] || {},
+          appliedColumns,
+          rejectedColumns,
         ),
-      [] as Indicator[],
-    );
+      ),
+    [] as Indicator[],
+  );
   indicators.sort((a, b) => a.name.localeCompare(b.name));
+  cachedIndicatorsForChart[chartId] = indicators;
+  cachedDashboardFilterDataForChart[chartId] = {
+    appliedColumns,
+    rejectedColumns,
+    matchingFilters,
+    matchingDatasources,
+  };
   return indicators;
 };
 
+const cachedNativeIndicatorsForChart = {};
+let cachedNativeFilterDataForChart: any = {};
+const defaultChartConfig = {};
 export const selectNativeIndicatorsForChart = (
   nativeFilters: Filters,
   dataMask: DataMaskStateWithId,
   chartId: number,
   chart: any,
   dashboardLayout: Layout,
-  chartConfiguration: ChartConfiguration = {},
+  chartConfiguration: ChartConfiguration = defaultChartConfig,
 ): Indicator[] => {
   const appliedColumns = getAppliedColumns(chart);
   const rejectedColumns = getRejectedColumns(chart);
 
+  const cachedFilterData = cachedNativeFilterDataForChart[chartId];
+  if (
+    cachedNativeIndicatorsForChart[chartId] &&
+    areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
+    areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
+    cachedNativeFilterDataForChart?.nativeFilters === nativeFilters &&
+    cachedNativeFilterDataForChart?.dashboardLayout === dashboardLayout &&
+    cachedNativeFilterDataForChart?.chartConfiguration === chartConfiguration &&
+    cachedNativeFilterDataForChart?.dataMask === dataMask
+  ) {
+    return cachedNativeIndicatorsForChart[chartId];
+  }
   const getStatus = ({
     label,
     column,
@@ -283,5 +324,18 @@
       })
       .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied);
   }
-  return crossFilterIndicators.concat(nativeFilterIndicators);
+  const indicators = crossFilterIndicators.concat(nativeFilterIndicators);
+  cachedNativeIndicatorsForChart[chartId] = indicators;
+  cachedNativeFilterDataForChart = {
+    ...cachedNativeFilterDataForChart,
+    nativeFilters,
+    dashboardLayout,
+    chartConfiguration,
+    dataMask,
+  };
+  cachedNativeFilterDataForChart[chartId] = {
+    appliedColumns,
+    rejectedColumns,
+  };
+  return indicators;
 };
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index a0cdb94..9b42c81 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { FC } from 'react';
+import React, { FC, useMemo } from 'react';
 import { styled, t } from '@superset-ui/core';
 import { Tooltip } from 'src/components/Tooltip';
 import { useDispatch, useSelector } from 'react-redux';
@@ -89,6 +89,14 @@
     state => state.dataMask[slice?.slice_id]?.filterState?.value,
   );
 
+  const indicator = useMemo(
+    () => ({
+      value: crossFilterValue,
+      name: t('Emitted values'),
+    }),
+    [crossFilterValue],
+  );
+
   return (
     <div className="chart-header" data-test="slice-header" ref={innerRef}>
       <div className="header-title">
@@ -139,10 +147,7 @@
                 placement="top"
                 title={
                   <FilterIndicator
-                    indicator={{
-                      value: crossFilterValue,
-                      name: t('Emitted values'),
-                    }}
+                    indicator={indicator}
                     text={t('Click to clear emitted filters')}
                   />
                 }