fix(frontend): memoize inline JSX objects for performance (#87)

- MovieIndexTable: memoize itemData, move row flex styles to CSS
- MovieIndexOverviews: memoize itemData, extract listStyle constant
- MovieIndexOverview: memoize elementStyle and infoStyle
- CircularProgressBar: memoize containerStyle and circleStyle

Reduces unnecessary re-renders in virtualized lists and frequently
rendered components.

Closes #41

Co-authored-by: admin <admin@ardentleatherworks.com>
This commit is contained in:
Cody Kickertz 2025-12-19 20:01:07 -06:00 committed by GitHub
parent 80c364110c
commit 1230212df8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 78 additions and 52 deletions

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styles from './CircularProgressBar.css';
interface CircularProgressBarProps {
@ -59,15 +59,24 @@ function CircularProgressBar({
[]
);
const containerStyle = useMemo(() => {
return {
width: sizeInPixels,
height: sizeInPixels,
lineHeight: sizeInPixels,
};
}, [sizeInPixels]);
const circleStyle = useMemo(() => {
return {
stroke: strokeColor,
strokeWidth,
strokeDashoffset,
};
}, [strokeColor, strokeWidth, strokeDashoffset]);
return (
<div
className={containerClassName}
style={{
width: sizeInPixels,
height: sizeInPixels,
lineHeight: sizeInPixels,
}}
>
<div className={containerClassName} style={containerStyle}>
<svg
className={className}
version="1.1"
@ -81,11 +90,7 @@ function CircularProgressBar({
cx={center}
cy={center}
strokeDasharray={circumference}
style={{
stroke: strokeColor,
strokeWidth,
strokeDashoffset,
}}
style={circleStyle}
/>
</svg>

View file

@ -124,10 +124,12 @@ function MovieIndexOverview(props: Readonly<MovieIndexOverviewProps>) {
const link = `/movie/${tmdbId}`;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
};
const elementStyle = useMemo(() => {
return {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
};
}, [posterWidth, posterHeight]);
const contentHeight = useMemo(() => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
@ -135,6 +137,10 @@ function MovieIndexOverview(props: Readonly<MovieIndexOverviewProps>) {
return rowHeight - padding;
}, [rowHeight, isSmallScreen]);
const infoStyle = useMemo(() => {
return { maxHeight: contentHeight };
}, [contentHeight]);
const overviewHeight = contentHeight - titleRowHeight;
return (
@ -175,7 +181,7 @@ function MovieIndexOverview(props: Readonly<MovieIndexOverviewProps>) {
/>
</div>
<div className={styles.info} style={{ maxHeight: contentHeight }}>
<div className={styles.info} style={infoStyle}>
<div className={styles.titleRow}>
<Link className={styles.title} to={link}>
{title}

View file

@ -23,6 +23,12 @@ const bodyPaddingSmallScreen = Number.parseInt(
dimensions.pageContentBodyPaddingSmallScreen
);
const listStyle = {
width: '100%',
height: '100%',
overflow: 'none',
} as const;
interface RowItemData {
items: Movie[];
sortKey: string;
@ -179,28 +185,36 @@ function MovieIndexOverviews(props: Readonly<MovieIndexOverviewsProps>) {
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
const itemData = useMemo(() => {
return {
items,
sortKey,
posterWidth,
posterHeight,
rowHeight,
isSelectMode,
isSmallScreen,
};
}, [
items,
sortKey,
posterWidth,
posterHeight,
rowHeight,
isSelectMode,
isSmallScreen,
]);
return (
<div ref={measureRef}>
<List<RowItemData>
ref={listRef}
style={{
width: '100%',
height: '100%',
overflow: 'none',
}}
style={listStyle}
width={size.width}
height={size.height}
itemCount={items.length}
itemSize={rowHeight}
itemData={{
items,
sortKey,
posterWidth,
posterHeight,
rowHeight,
isSelectMode,
isSmallScreen,
}}
itemData={itemData}
>
{Row}
</List>

View file

@ -3,6 +3,8 @@
}
.row {
display: flex;
justify-content: space-between;
transition: background-color 500ms;
&:hover {

View file

@ -20,6 +20,12 @@ const bodyPaddingSmallScreen = Number.parseInt(
dimensions.pageContentBodyPaddingSmallScreen
);
const listStyle = {
width: '100%',
height: '100%',
overflow: 'none',
} as const;
interface RowItemData {
items: Movie[];
sortKey: string;
@ -53,14 +59,7 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const movie = items[index];
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
...style,
}}
className={styles.row}
>
<div style={style} className={styles.row}>
<MovieIndexRow
movieId={movie.id}
sortKey={sortKey}
@ -167,6 +166,15 @@ function MovieIndexTable(props: Readonly<MovieIndexTableProps>) {
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
const itemData = useMemo(() => {
return {
items,
sortKey,
columns,
isSelectMode,
};
}, [items, sortKey, columns, isSelectMode]);
return (
<div ref={measureRef}>
<Scroller className={styles.tableScroller} scrollDirection="horizontal">
@ -178,21 +186,12 @@ function MovieIndexTable(props: Readonly<MovieIndexTableProps>) {
/>
<List<RowItemData>
ref={listRef}
style={{
width: '100%',
height: '100%',
overflow: 'none',
}}
style={listStyle}
width={size.width}
height={size.height}
itemCount={items.length}
itemSize={rowHeight}
itemData={{
items,
sortKey,
columns,
isSelectMode,
}}
itemData={itemData}
>
{Row}
</List>