mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
perf: Reduced unnecessary re-renders in the channels table. Removed three Zustand store subscriptions from ChannelsTable that caused a full component re-render on every unrelated store update: the entire channels map, selectedProfileChannels, and selectedProfileChannelIds (these were superseded when ChannelEnabledSwitch was refactored to subscribe to the store directly). Narrowed the channelIds subscription to a stable boolean hasChannels so only the empty↔non-empty transition triggers a re-render instead of every channel add/remove. Added a custom React.memo comparator to ChannelRowActions that compares row.original.id rather than the full TanStack Table row object (which is recreated each render cycle), preventing every action cell from re-rendering on table updates. Memoized getChannelURL, createRecording, and handleWatchStream with useCallback for stable column cell references.
This commit is contained in:
parent
3586dba677
commit
3bdbf30c21
3 changed files with 38 additions and 100 deletions
|
|
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Performance
|
||||
|
||||
- Reduced unnecessary re-renders in the channels table. Removed three Zustand store subscriptions from `ChannelsTable` that caused a full component re-render on every unrelated store update: the entire `channels` map, `selectedProfileChannels`, and `selectedProfileChannelIds` (these were superseded when `ChannelEnabledSwitch` was refactored to subscribe to the store directly). Narrowed the `channelIds` subscription to a stable boolean `hasChannels` so only the empty↔non-empty transition triggers a re-render instead of every channel add/remove. Added a custom `React.memo` comparator to `ChannelRowActions` that compares `row.original.id` rather than the full TanStack Table row object (which is recreated each render cycle), preventing every action cell from re-rendering on table updates. Memoized `getChannelURL`, `createRecording`, and `handleWatchStream` with `useCallback` for stable column cell references.
|
||||
- Eliminated repeated DB queries in the `ts_proxy` hot path. `StreamManager`, `StreamGenerator`, and `ProxyServer` were each calling `Channel.objects.get(uuid=...)` on every retry, reconnect, failover, and buffering event solely to retrieve `channel.name` for log events. `StreamManager` and `StreamGenerator` now fetch the channel name once at construction via a lightweight `values_list` query and store it as `self.channel_name`. `ProxyServer` caches the name in a `_channel_names` dict keyed by channel ID at channel-start time and pops it at channel-stop time. (Fixes #1138)
|
||||
|
||||
## [0.23.0] - 2026-04-17
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ import {
|
|||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import API from '../../api';
|
||||
import ChannelForm from '../forms/Channel';
|
||||
import ChannelBatchForm from '../forms/ChannelBatch';
|
||||
import RecordingForm from '../forms/Recording';
|
||||
import { useDebounce, copyToClipboard } from '../../utils';
|
||||
import logo from '../../images/logo.png';
|
||||
import useVideoStore from '../../store/useVideoStore';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
import {
|
||||
|
|
@ -65,7 +63,6 @@ import {
|
|||
Tooltip,
|
||||
Skeleton,
|
||||
} from '@mantine/core';
|
||||
import { getCoreRowModel, flexRender } from '@tanstack/react-table';
|
||||
import './table.css';
|
||||
import useChannelsTableStore from '../../store/channelsTable';
|
||||
import ChannelTableStreams from './ChannelTableStreams';
|
||||
|
|
@ -243,7 +240,12 @@ const ChannelRowActions = React.memo(
|
|||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
},
|
||||
// Custom comparator: only re-render when the actual channel changes.
|
||||
// The row object is a new TanStack Table reference on each render, but
|
||||
// row.original.id is stable. Callbacks read fresh data at call time.
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.row.original.id === nextProps.row.original.id
|
||||
);
|
||||
|
||||
const ChannelsTable = ({ onReady }) => {
|
||||
|
|
@ -291,22 +293,16 @@ const ChannelsTable = ({ onReady }) => {
|
|||
const sorting = useChannelsTableStore((s) => s.sorting);
|
||||
const setSorting = useChannelsTableStore((s) => s.setSorting);
|
||||
const totalCount = useChannelsTableStore((s) => s.totalCount);
|
||||
const setChannelStreams = useChannelsTableStore((s) => s.setChannelStreams);
|
||||
const allRowIds = useChannelsTableStore((s) => s.allQueryIds);
|
||||
const setAllRowIds = useChannelsTableStore((s) => s.setAllQueryIds);
|
||||
const isUnlocked = useChannelsTableStore((s) => s.isUnlocked);
|
||||
|
||||
// store/channels
|
||||
const channels = useChannelsStore((s) => s.channels);
|
||||
const channelIds = useChannelsStore((s) => s.channelIds);
|
||||
const hasChannels = useChannelsStore((s) => s.channelIds.length > 0);
|
||||
const profiles = useChannelsStore((s) => s.profiles);
|
||||
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
|
||||
const [tablePrefs, setTablePrefs] = useLocalStorage('channel-table-prefs', {
|
||||
const [, setTablePrefs] = useLocalStorage('channel-table-prefs', {
|
||||
pageSize: 50,
|
||||
});
|
||||
const selectedProfileChannels = useChannelsStore(
|
||||
(s) => s.profiles[selectedProfileId]?.channels
|
||||
);
|
||||
|
||||
// store/settings
|
||||
const env_mode = useSettingsStore((s) => s.environment.env_mode);
|
||||
|
|
@ -316,14 +312,6 @@ const ChannelsTable = ({ onReady }) => {
|
|||
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
|
||||
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
||||
|
||||
/**
|
||||
* useMemo
|
||||
*/
|
||||
const selectedProfileChannelIds = useMemo(
|
||||
() => new Set(selectedProfileChannels),
|
||||
[selectedProfileChannels]
|
||||
);
|
||||
|
||||
/**
|
||||
* useState
|
||||
*/
|
||||
|
|
@ -331,9 +319,6 @@ const ChannelsTable = ({ onReady }) => {
|
|||
const [channelModalOpen, setChannelModalOpen] = useState(false);
|
||||
const [channelBatchModalOpen, setChannelBatchModalOpen] = useState(false);
|
||||
const [recordingModalOpen, setRecordingModalOpen] = useState(false);
|
||||
const [selectedProfile, setSelectedProfile] = useState(
|
||||
profiles[selectedProfileId]
|
||||
);
|
||||
const [showDisabled, setShowDisabled] = useState(true);
|
||||
const [showOnlyStreamlessChannels, setShowOnlyStreamlessChannels] =
|
||||
useState(false);
|
||||
|
|
@ -345,7 +330,7 @@ const ChannelsTable = ({ onReady }) => {
|
|||
channel_group: '',
|
||||
epg: '',
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [, setIsLoading] = useState(true);
|
||||
|
||||
const [hdhrUrl, setHDHRUrl] = useState(hdhrUrlBase);
|
||||
const [epgUrl, setEPGUrl] = useState(epgUrlBase);
|
||||
|
|
@ -490,7 +475,7 @@ const ChannelsTable = ({ onReady }) => {
|
|||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const [results, ids] = await Promise.all([
|
||||
const [, ids] = await Promise.all([
|
||||
API.queryChannels(params),
|
||||
API.getAllChannelIds(params),
|
||||
]);
|
||||
|
|
@ -673,37 +658,38 @@ const ChannelsTable = ({ onReady }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const createRecording = (channel) => {
|
||||
const createRecording = useCallback((channel) => {
|
||||
console.log(`Recording channel ID: ${channel.id}`);
|
||||
setChannel(channel);
|
||||
setRecordingModalOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getChannelURL = (channel) => {
|
||||
// Make sure we're using the channel UUID consistently
|
||||
if (!channel || !channel.uuid) {
|
||||
console.error('Invalid channel object or missing UUID:', channel);
|
||||
return '';
|
||||
}
|
||||
const getChannelURL = useCallback(
|
||||
(channel) => {
|
||||
// Make sure we're using the channel UUID consistently
|
||||
if (!channel || !channel.uuid) {
|
||||
console.error('Invalid channel object or missing UUID:', channel);
|
||||
return '';
|
||||
}
|
||||
|
||||
const uri = `/proxy/ts/stream/${channel.uuid}`;
|
||||
let channelUrl = `${window.location.protocol}//${window.location.host}${uri}`;
|
||||
if (env_mode == 'dev') {
|
||||
channelUrl = `${window.location.protocol}//${window.location.hostname}:5656${uri}`;
|
||||
}
|
||||
const uri = `/proxy/ts/stream/${channel.uuid}`;
|
||||
let channelUrl = `${window.location.protocol}//${window.location.host}${uri}`;
|
||||
if (env_mode == 'dev') {
|
||||
channelUrl = `${window.location.protocol}//${window.location.hostname}:5656${uri}`;
|
||||
}
|
||||
|
||||
return channelUrl;
|
||||
};
|
||||
return channelUrl;
|
||||
},
|
||||
[env_mode]
|
||||
);
|
||||
|
||||
const handleWatchStream = (channel) => {
|
||||
// Add additional logging to help debug issues
|
||||
console.log(
|
||||
`Watching stream for channel: ${channel.name} (${channel.id}), UUID: ${channel.uuid}`
|
||||
);
|
||||
const url = getChannelURL(channel);
|
||||
console.log(`Stream URL: ${url}`);
|
||||
showVideo(url, 'live', { name: channel.name });
|
||||
};
|
||||
const handleWatchStream = useCallback(
|
||||
(channel) => {
|
||||
const url = getChannelURL(channel);
|
||||
showVideo(url, 'live', { name: channel.name });
|
||||
},
|
||||
[getChannelURL, showVideo]
|
||||
);
|
||||
|
||||
const onRowSelectionChange = (newSelection) => {
|
||||
setSelectedChannelIds(newSelection);
|
||||
|
|
@ -861,8 +847,6 @@ const ChannelsTable = ({ onReady }) => {
|
|||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedProfile(profiles[selectedProfileId]);
|
||||
|
||||
const profileString =
|
||||
selectedProfileId != '0' ? `/${profiles[selectedProfileId].name}` : '';
|
||||
setHDHRUrl(`${hdhrUrlBase}${profileString}`);
|
||||
|
|
@ -1524,12 +1508,12 @@ const ChannelsTable = ({ onReady }) => {
|
|||
|
||||
{/* Table or ghost empty state inside Paper */}
|
||||
<Box>
|
||||
{channelsTableLength === 0 && channelIds.length === 0 && (
|
||||
{channelsTableLength === 0 && !hasChannels && (
|
||||
<ChannelsTableOnboarding editChannel={editChannel} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{(channelsTableLength > 0 || channelIds.length > 0) && (
|
||||
{(channelsTableLength > 0 || hasChannels) && (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
|
|
|
|||
|
|
@ -43,53 +43,6 @@ const CustomTableBody = ({
|
|||
}, [rows]);
|
||||
|
||||
const renderTableBodyContents = () => {
|
||||
const virtualized = false;
|
||||
|
||||
if (virtualized) {
|
||||
return (
|
||||
<Box
|
||||
className="tbody"
|
||||
style={{ flex: 1, ...(tableBodyProps && tableBodyProps()) }}
|
||||
>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => {
|
||||
const getItemSize = (index) => {
|
||||
const row = rows[index];
|
||||
const isExpanded = expandedRowIds.includes(row.original.id);
|
||||
console.log(isExpanded);
|
||||
|
||||
// Default row height
|
||||
let rowHeight = 28;
|
||||
|
||||
if (isExpanded && getExpandedRowHeight) {
|
||||
// If row is expanded, adjust the height to be larger (based on your logic)
|
||||
// You can get this height from your state, or calculate based on number of items in the expanded row
|
||||
rowHeight += getExpandedRowHeight(row); // This function would calculate the expanded row's height
|
||||
}
|
||||
|
||||
return rowHeight;
|
||||
};
|
||||
|
||||
return (
|
||||
<List
|
||||
height={height}
|
||||
itemCount={rows.length}
|
||||
itemSize={getItemSize}
|
||||
width="100%"
|
||||
overscanCount={10}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const row = rows[index];
|
||||
return renderTableBodyRow(row, index, style);
|
||||
}}
|
||||
</List>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="tbody" style={{ flex: 1 }}>
|
||||
{rows.map((row, index) => renderTableBodyRow(row, index))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue