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:
SergeantPanda 2026-04-18 20:23:14 -05:00
parent 3586dba677
commit 3bdbf30c21
3 changed files with 38 additions and 100 deletions

View file

@ -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

View file

@ -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',

View file

@ -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))}