perf: reduce DBSearchPage and DBChartExplorerPage rerenders from typing in search input (#924)

This commit is contained in:
Aaron Knudtson 2025-06-20 10:12:23 -04:00 committed by GitHub
parent b427ae3526
commit d1f4184536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 122 additions and 62 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
perf: improve performance on chart page and search page

View file

@ -1,5 +1,6 @@
import {
FormEvent,
FormEventHandler,
useCallback,
useEffect,
useMemo,
@ -964,14 +965,18 @@ function DBSearchPage() {
const { data: aliasMap } = useAliasMapFromChartConfig(dbSqlRowTableConfig);
const aliasWith = Object.entries(aliasMap ?? {}).map(([key, value]) => ({
name: key,
sql: {
sql: value,
params: {},
},
isSubquery: false,
}));
const aliasWith = useMemo(
() =>
Object.entries(aliasMap ?? {}).map(([key, value]) => ({
name: key,
sql: {
sql: value,
params: {},
},
isSubquery: false,
})),
[aliasMap],
);
const histogramTimeChartConfig = useMemo(() => {
if (chartConfig == null) {
@ -1006,6 +1011,60 @@ function DBSearchPage() {
};
}, [chartConfig, searchedSource, aliasWith, searchedTimeRange]);
const onFormSubmit = useCallback<FormEventHandler<HTMLFormElement>>(
e => {
e.preventDefault();
onSubmit();
return false;
},
[onSubmit],
);
const handleTimeRangeSelect = useCallback(
(d1: Date, d2: Date) => {
onTimeRangeSelect(d1, d2);
setIsLive(false);
},
[onTimeRangeSelect],
);
const onTimeChartError = useCallback(
(error: Error | ClickHouseQueryError) =>
setQueryErrors(prev => ({
...prev,
DBTimeChart: error,
})),
[setQueryErrors],
);
const filtersChartConfig = useMemo<ChartConfigWithDateRange>(() => {
const overrides = {
orderBy: undefined,
dateRange: searchedTimeRange,
with: aliasWith,
} as const;
return chartConfig
? {
...chartConfig,
...overrides,
}
: {
timestampValueExpression: '',
connection: '',
from: {
databaseName: '',
tableName: '',
},
where: '',
select: '',
...overrides,
};
}, [chartConfig, searchedTimeRange, aliasWith]);
const openNewSourceModal = useCallback(() => {
setNewSourceModalOpened(true);
}, []);
return (
<Flex direction="column" h="100vh" style={{ overflow: 'hidden' }}>
{!IS_LOCAL_MODE && isAlertModalOpen && (
@ -1017,13 +1076,7 @@ function DBSearchPage() {
/>
)}
<OnboardingModal />
<form
onSubmit={e => {
e.preventDefault();
onSubmit();
return false;
}}
>
<form onSubmit={onFormSubmit}>
{/* <DevTool control={control} /> */}
<Flex gap="sm" px="sm" pt="sm" wrap="nowrap">
<Group gap="4px" wrap="nowrap">
@ -1032,9 +1085,7 @@ function DBSearchPage() {
size="xs"
control={control}
name="source"
onCreate={() => {
setNewSourceModalOpened(true);
}}
onCreate={openNewSourceModal}
/>
<ActionIcon
variant="subtle"
@ -1297,12 +1348,7 @@ function DBSearchPage() {
isLive={isLive}
analysisMode={analysisMode}
setAnalysisMode={setAnalysisMode}
chartConfig={{
...chartConfig,
orderBy: undefined,
dateRange: searchedTimeRange,
with: aliasWith,
}}
chartConfig={filtersChartConfig}
sourceId={inputSourceObj?.id}
showDelta={!!searchedSource?.durationExpression}
{...searchFilters}
@ -1344,16 +1390,8 @@ function DBSearchPage() {
enabled={isReady}
showDisplaySwitcher={false}
queryKeyPrefix={QUERY_KEY_PREFIX}
onTimeRangeSelect={(d1, d2) => {
onTimeRangeSelect(d1, d2);
setIsLive(false);
}}
onError={error =>
setQueryErrors(prev => ({
...prev,
DBTimeChart: error,
}))
}
onTimeRangeSelect={handleTimeRangeSelect}
onError={onTimeChartError}
/>
</Box>
)}
@ -1464,16 +1502,8 @@ function DBSearchPage() {
enabled={isReady}
showDisplaySwitcher={false}
queryKeyPrefix={QUERY_KEY_PREFIX}
onTimeRangeSelect={(d1, d2) => {
onTimeRangeSelect(d1, d2);
setIsLive(false);
}}
onError={error =>
setQueryErrors(prev => ({
...prev,
DBTimeChart: error,
}))
}
onTimeRangeSelect={handleTimeRangeSelect}
onError={onTimeChartError}
/>
</Box>
)}

View file

@ -1,3 +1,4 @@
import { memo } from 'react';
import { useController, UseControllerProps } from 'react-hook-form';
import { Granularity } from './ChartUtils';
@ -63,7 +64,9 @@ export default function GranularityPicker({
);
}
export function GranularityPickerControlled(props: UseControllerProps<any>) {
export function GranularityPickerControlledComponent(
props: UseControllerProps<any>,
) {
const {
field,
fieldState: { invalid, isTouched, isDirty },
@ -72,3 +75,7 @@ export function GranularityPickerControlled(props: UseControllerProps<any>) {
return <GranularityPicker value={field.value} onChange={field.onChange} />;
}
export const GranularityPickerControlled = memo(
GranularityPickerControlledComponent,
);

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import {
Control,
Controller,
@ -106,7 +106,7 @@ const NumberFormatInputControlled = ({
);
};
function ChartSeriesEditor({
function ChartSeriesEditorComponent({
control,
databaseName,
dateRange,
@ -124,9 +124,9 @@ function ChartSeriesEditor({
databaseName: string;
dateRange?: DateRange['dateRange'];
connectionId?: string;
index?: number;
index: number;
namePrefix: string;
onRemoveSeries: () => void;
onRemoveSeries: (index: number) => void;
onSubmit: () => void;
setValue: UseFormSetValue<any>;
showGroupBy: boolean;
@ -168,7 +168,7 @@ function ChartSeriesEditor({
variant="subtle"
color="gray"
size="xs"
onClick={() => onRemoveSeries()}
onClick={() => onRemoveSeries(index)}
>
<i className="bi bi-trash me-2" />
Remove Series
@ -286,6 +286,7 @@ function ChartSeriesEditor({
</>
);
}
const ChartSeriesEditor = memo(ChartSeriesEditorComponent);
// Autocomplete can focus on column/map keys
@ -337,7 +338,11 @@ export default function EditTimeChartForm({
resolver: zodResolver(zSavedChartConfig),
});
const { fields, append, remove } = useFieldArray({
const {
fields,
append,
remove: removeSeries,
} = useFieldArray({
control: control as Control<SavedChartConfigWithSelectArray>,
name: 'select',
});
@ -604,7 +609,7 @@ export default function EditTimeChartForm({
index={index}
key={field.id}
namePrefix={`select.${index}.`}
onRemoveSeries={() => remove(index)}
onRemoveSeries={removeSeries}
onSubmit={onSubmit}
setValue={setValue}
connectionId={tableSource?.connection}

View file

@ -898,7 +898,7 @@ export function selectColumnMapWithoutAdditionalKeys(
);
}
export function DBSqlRowTable({
function DBSqlRowTableComponent({
config,
sourceId,
onError,
@ -977,7 +977,10 @@ export function DBSqlRowTable({
});
}, [data, objectTypeColumns, columnMap]);
const aliasMap = chSqlToAliasMap(data?.chSql ?? { sql: '', params: {} });
const aliasMap = useMemo(
() => chSqlToAliasMap(data?.chSql ?? { sql: '', params: {} }),
[data],
);
const getRowWhere = useRowWhere({ meta: data?.meta, aliasMap });
@ -1110,3 +1113,4 @@ export function DBSqlRowTable({
</>
);
}
export const DBSqlRowTable = memo(DBSqlRowTableComponent);

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
import {
Box,
@ -351,7 +351,7 @@ export const FilterGroup = ({
);
};
export const DBSearchPageFilters = ({
const DBSearchPageFiltersComponent = ({
filters: filterState,
clearAllFilters,
clearFilter,
@ -671,3 +671,5 @@ export const DBSearchPageFilters = ({
</Box>
);
};
export const DBSearchPageFilters = memo(DBSearchPageFiltersComponent);

View file

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { memo, useMemo, useState } from 'react';
import Link from 'next/link';
import cx from 'classnames';
import { add } from 'date-fns';
@ -22,7 +22,7 @@ import { SQLPreview } from './ChartSQLPreview';
// TODO: Support clicking in to view matched events
export function DBTimeChart({
function DBTimeChartComponent({
config,
enabled = true,
logReferenceTimestamp,
@ -322,3 +322,5 @@ export function DBTimeChart({
</div>
);
}
export const DBTimeChart = memo(DBTimeChartComponent);

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useController, UseControllerProps } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import {
@ -350,7 +350,7 @@ export default function SQLInlineEditor({
);
}
export function SQLInlineEditorControlled({
function SQLInlineEditorControlledComponent({
placeholder,
filterField,
additionalSuggestions,
@ -381,3 +381,6 @@ export function SQLInlineEditorControlled({
/>
);
}
export const SQLInlineEditorControlled = memo(
SQLInlineEditorControlledComponent,
);

View file

@ -1,11 +1,11 @@
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import { UseControllerProps } from 'react-hook-form';
import SelectControlled from '@/components/SelectControlled';
import { HDX_LOCAL_DEFAULT_SOURCES } from '@/config';
import { useSources } from '@/source';
export function SourceSelectControlled({
function SourceSelectControlledComponent({
size,
onCreate,
...props
@ -46,3 +46,5 @@ export function SourceSelectControlled({
/>
);
}
export const SourceSelectControlled = memo(SourceSelectControlledComponent);