Optimize Tag List Page Performance (#6398)

This commit is contained in:
CJ
2025-12-10 20:59:19 -06:00
committed by GitHub
parent ebfe5c4b5c
commit f1e54bfc73
8 changed files with 77 additions and 19 deletions

View File

@@ -57,3 +57,31 @@ fragment SelectTagData on Tag {
updated_at updated_at
} }
} }
# Optimized fragment for tag list page - excludes expensive recursive *_count_all fields
fragment TagListData on Tag {
id
name
sort_name
description
aliases
ignore_auto_tag
favorite
image_path
# Direct counts only - no recursive depth queries
scene_count
scene_marker_count
image_count
gallery_count
performer_count
studio_count
group_count
parents {
...SlimTagData
}
children {
...SlimTagData
}
}

View File

@@ -25,3 +25,13 @@ query FindTagsForSelect(
} }
} }
} }
# Optimized query for tag list page - uses TagListData fragment without recursive counts
query FindTagsForList($filter: FindFilterType, $tag_filter: TagFilterType) {
findTags(filter: $filter, tag_filter: $tag_filter) {
count
tags {
...TagListData
}
}
}

View File

@@ -55,7 +55,7 @@ function Tags(props: {
} }
interface IListOperationProps { interface IListOperationProps {
selected: GQL.TagDataFragment[]; selected: (GQL.TagDataFragment | GQL.TagListDataFragment)[];
onClose: (applied: boolean) => void; onClose: (applied: boolean) => void;
} }
@@ -134,7 +134,7 @@ export const EditTagsDialog: React.FC<IListOperationProps> = (
let updateChildTagIds: string[] = []; let updateChildTagIds: string[] = [];
let first = true; let first = true;
state.forEach((tag: GQL.TagDataFragment) => { state.forEach((tag: GQL.TagDataFragment | GQL.TagListDataFragment) => {
getAggregateStateObject(updateState, tag, tagFields, first); getAggregateStateObject(updateState, tag, tagFields, first);
const thisParents = (tag.parents ?? []).map((t) => t.id).sort(); const thisParents = (tag.parents ?? []).map((t) => t.id).sort();

View File

@@ -14,7 +14,7 @@ import cx from "classnames";
import { useTagUpdate } from "src/core/StashService"; import { useTagUpdate } from "src/core/StashService";
interface IProps { interface IProps {
tag: GQL.TagDataFragment; tag: GQL.TagDataFragment | GQL.TagListDataFragment;
cardWidth?: number; cardWidth?: number;
zoomIndex: number; zoomIndex: number;
selecting?: boolean; selecting?: boolean;

View File

@@ -7,7 +7,7 @@ import {
import { TagCard } from "./TagCard"; import { TagCard } from "./TagCard";
interface ITagCardGrid { interface ITagCardGrid {
tags: GQL.TagDataFragment[]; tags: (GQL.TagDataFragment | GQL.TagListDataFragment)[];
selectedIds: Set<string>; selectedIds: Set<string>;
zoomIndex: number; zoomIndex: number;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;

View File

@@ -8,9 +8,9 @@ import { Button } from "react-bootstrap";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
queryFindTags, queryFindTagsForList,
mutateMetadataAutoTag, mutateMetadataAutoTag,
useFindTags, useFindTagsForList,
useTagDestroy, useTagDestroy,
useTagsDestroy, useTagsDestroy,
} from "src/core/StashService"; } from "src/core/StashService";
@@ -27,11 +27,11 @@ import { TagCardGrid } from "./TagCardGrid";
import { EditTagsDialog } from "./EditTagsDialog"; import { EditTagsDialog } from "./EditTagsDialog";
import { View } from "../List/views"; import { View } from "../List/views";
function getItems(result: GQL.FindTagsQueryResult) { function getItems(result: GQL.FindTagsForListQueryResult) {
return result?.data?.findTags?.tags ?? []; return result?.data?.findTags?.tags ?? [];
} }
function getCount(result: GQL.FindTagsQueryResult) { function getCount(result: GQL.FindTagsForListQueryResult) {
return result?.data?.findTags?.count ?? 0; return result?.data?.findTags?.count ?? 0;
} }
@@ -43,7 +43,7 @@ interface ITagList {
export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => { export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
const Toast = useToast(); const Toast = useToast();
const [deletingTag, setDeletingTag] = const [deletingTag, setDeletingTag] =
useState<Partial<GQL.TagDataFragment> | null>(null); useState<Partial<GQL.TagListDataFragment> | null>(null);
const filterMode = GQL.FilterMode.Tags; const filterMode = GQL.FilterMode.Tags;
const view = View.Tags; const view = View.Tags;
@@ -79,7 +79,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
]; ];
function addKeybinds( function addKeybinds(
result: GQL.FindTagsQueryResult, result: GQL.FindTagsForListQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
Mousetrap.bind("p r", () => { Mousetrap.bind("p r", () => {
@@ -92,7 +92,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
} }
async function viewRandom( async function viewRandom(
result: GQL.FindTagsQueryResult, result: GQL.FindTagsForListQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
// query for a random tag // query for a random tag
@@ -103,7 +103,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
const filterCopy = cloneDeep(filter); const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await queryFindTags(filterCopy); const singleResult = await queryFindTagsForList(filterCopy);
if (singleResult.data.findTags.tags.length === 1) { if (singleResult.data.findTags.tags.length === 1) {
const { id } = singleResult.data.findTags.tags[0]; const { id } = singleResult.data.findTags.tags[0];
// navigate to the tag page // navigate to the tag page
@@ -122,7 +122,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
async function onAutoTag(tag: GQL.TagDataFragment) { async function onAutoTag(tag: GQL.TagListDataFragment) {
if (!tag) return; if (!tag) return;
try { try {
await mutateMetadataAutoTag({ tags: [tag.id] }); await mutateMetadataAutoTag({ tags: [tag.id] });
@@ -139,7 +139,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
children: deletingTag?.children ?? [], children: deletingTag?.children ?? [],
}; };
await deleteTag(); await deleteTag();
tagRelationHook(deletingTag as GQL.TagDataFragment, oldRelations, { tagRelationHook(deletingTag as GQL.TagListDataFragment, oldRelations, {
parents: [], parents: [],
children: [], children: [],
}); });
@@ -160,7 +160,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
} }
function renderContent( function renderContent(
result: GQL.FindTagsQueryResult, result: GQL.FindTagsForListQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
@@ -324,14 +324,14 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
} }
function renderEditDialog( function renderEditDialog(
selectedTags: GQL.TagDataFragment[], selectedTags: GQL.TagListDataFragment[],
onClose: (confirmed: boolean) => void onClose: (confirmed: boolean) => void
) { ) {
return <EditTagsDialog selected={selectedTags} onClose={onClose} />; return <EditTagsDialog selected={selectedTags} onClose={onClose} />;
} }
function renderDeleteDialog( function renderDeleteDialog(
selectedTags: GQL.TagDataFragment[], selectedTags: GQL.TagListDataFragment[],
onClose: (confirmed: boolean) => void onClose: (confirmed: boolean) => void
) { ) {
return ( return (
@@ -357,7 +357,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
return ( return (
<ItemListContext <ItemListContext
filterMode={filterMode} filterMode={filterMode}
useResult={useFindTags} useResult={useFindTagsForList}
getItems={getItems} getItems={getItems}
getCount={getCount} getCount={getCount}
alterQuery={alterQuery} alterQuery={alterQuery}

View File

@@ -429,6 +429,16 @@ export const useFindTags = (filter?: ListFilterModel) =>
}, },
}); });
// Optimized query for tag list page - excludes expensive recursive *_count_all fields
export const useFindTagsForList = (filter?: ListFilterModel) =>
GQL.useFindTagsForListQuery({
skip: filter === undefined,
variables: {
filter: filter?.makeFindFilter(),
tag_filter: filter?.makeFilter(),
},
});
export const queryFindTags = (filter: ListFilterModel) => export const queryFindTags = (filter: ListFilterModel) =>
client.query<GQL.FindTagsQuery>({ client.query<GQL.FindTagsQuery>({
query: GQL.FindTagsDocument, query: GQL.FindTagsDocument,
@@ -438,6 +448,16 @@ export const queryFindTags = (filter: ListFilterModel) =>
}, },
}); });
// Optimized query for tag list page
export const queryFindTagsForList = (filter: ListFilterModel) =>
client.query<GQL.FindTagsForListQuery>({
query: GQL.FindTagsForListDocument,
variables: {
filter: filter.makeFindFilter(),
tag_filter: filter.makeFilter(),
},
});
export const queryFindTagsByIDForSelect = (tagIDs: string[]) => export const queryFindTagsByIDForSelect = (tagIDs: string[]) =>
client.query<GQL.FindTagsForSelectQuery>({ client.query<GQL.FindTagsForSelectQuery>({
query: GQL.FindTagsForSelectDocument, query: GQL.FindTagsForSelectDocument,

View File

@@ -58,7 +58,7 @@ interface ITagRelationTuple {
} }
export const tagRelationHook = ( export const tagRelationHook = (
tag: GQL.SlimTagDataFragment | GQL.TagDataFragment, tag: GQL.SlimTagDataFragment | GQL.TagDataFragment | GQL.TagListDataFragment,
old: ITagRelationTuple, old: ITagRelationTuple,
updated: ITagRelationTuple updated: ITagRelationTuple
) => { ) => {