perf(reader): split progress into its own store to cut React commit storm (#4557)
setProgress was called multiple times per swipe burst, each call writing into readerStore.viewStates[key].progress. ~65 places in the reader subtree subscribed to useReaderStore() without a selector, so every setProgress fan-out re-rendered all of them -- even the 51 that didn't care about progress. On Android release builds this showed up as Layout = 9.8% and Function Call = 9.6% of main-thread self time in Chrome DevTools' Bottom-Up profile during a reading session. Fix: - New tiny store store/readerProgressStore.ts holds the per-book BookProgress map. setBookProgress only fires its own subscribers. - readerStore.setProgress now writes progress to the new store and only touches bookDataStore for the primary view (secondary parallel views shouldn't overwrite the shared config). - readerStore.getProgress is kept as a delegating facade so existing imperative call sites don't break. - Components / hooks that genuinely need to react to progress changes subscribe via the new useBookProgress(bookKey) hook. The handful of call sites that just want a one-shot read use getBookProgress(key) so they don't subscribe at all. - readerStore.clearViewState calls clearBookProgress so the map doesn't grow unbounded across book opens/closes. See store/readerProgressStore.ts header for the full rationale.
This commit is contained in:
@@ -4,6 +4,7 @@ import { IoChevronBack, IoChevronForward } from 'react-icons/io5';
|
||||
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookProgress } from '@/store/readerProgressStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { viewPagination } from '../hooks/usePagination';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
@@ -19,12 +20,17 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getProgress, getViewSettings, hoveredBookKey } = useReaderStore();
|
||||
const getBookData = useBookDataStore((s) => s.getBookData);
|
||||
const getView = useReaderStore((s) => s.getView);
|
||||
const getViewSettings = useReaderStore((s) => s.getViewSettings);
|
||||
// hoveredBookKey is reactive state — drives button visibility.
|
||||
const hoveredBookKey = useReaderStore((s) => s.hoveredBookKey);
|
||||
const bookData = getBookData(bookKey);
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
// Reactive: drives the aria-live "Page N" announcement and the
|
||||
// currentPage label on the buttons. Reads from readerProgressStore.
|
||||
const progress = useBookProgress(bookKey);
|
||||
const { section, pageinfo } = progress || {};
|
||||
const pageInfo = bookData?.isFixedLayout ? section : pageinfo;
|
||||
const currentPage = pageInfo?.current;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Trans } from 'react-i18next';
|
||||
import type { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookProgress } from '@/store/readerProgressStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { formatNumber, formatProgress, getReferencePageInfo } from '@/utils/progress';
|
||||
@@ -28,12 +29,15 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewSettings, getView } = useReaderStore();
|
||||
const getBookData = useBookDataStore((s) => s.getBookData);
|
||||
const getViewSettings = useReaderStore((s) => s.getViewSettings);
|
||||
const getView = useReaderStore((s) => s.getView);
|
||||
const view = getView(bookKey);
|
||||
const bookData = getBookData(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const progress = getProgress(bookKey);
|
||||
// Reactive: this is the on-screen footer that has to refresh on every
|
||||
// page turn. Reads from readerProgressStore only.
|
||||
const progress = useBookProgress(bookKey);
|
||||
const { section, pageinfo } = progress || {};
|
||||
|
||||
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookProgress } from '@/store/readerProgressStore';
|
||||
import { useAIChatStore } from '@/store/aiChatStore';
|
||||
import { aiLogger, createTauriAdapter } from '@/services/ai';
|
||||
import {
|
||||
@@ -289,7 +290,7 @@ const ThreadWrapper = ({
|
||||
const AIAssistant = ({ bookKey }: AIAssistantProps) => {
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const getBookData = useBookDataStore((s) => s.getBookData);
|
||||
const bookData = getBookData(bookKey);
|
||||
|
||||
const reedyRuntime = settings?.aiSettings?.reedy?.runtime ?? 'mvp';
|
||||
@@ -309,10 +310,11 @@ const LegacyAIAssistant = ({ bookKey }: AIAssistantProps) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getProgress, getView } = useReaderStore();
|
||||
const getBookData = useBookDataStore((s) => s.getBookData);
|
||||
const getView = useReaderStore((s) => s.getView);
|
||||
const bookData = getBookData(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
// Reactive: chat context follows the user's current reading position.
|
||||
const progress = useBookProgress(bookKey);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isIndexing, setIsIndexing] = useState(false);
|
||||
@@ -476,10 +478,12 @@ const LegacyAIAssistant = ({ bookKey }: AIAssistantProps) => {
|
||||
const ReedyAgentAssistantBridge = ({ bookKey }: AIAssistantProps) => {
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getProgress, getView } = useReaderStore();
|
||||
const getBookData = useBookDataStore((s) => s.getBookData);
|
||||
const getView = useReaderStore((s) => s.getView);
|
||||
const bookData = getBookData(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
// Reactive: agent runtime needs the latest reading position to seed
|
||||
// tool calls.
|
||||
const progress = useBookProgress(bookKey);
|
||||
|
||||
const bookHash = bookKey.split('-')[0] || '';
|
||||
const aiSettings = settings?.aiSettings;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookProgress } from '@/store/readerProgressStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
@@ -114,10 +115,17 @@ const expandRangeToSentence = (range: Range, doc: Document): Range => {
|
||||
const RSVPControl: React.FC<RSVPControlProps> = ({ bookKey, gridInsets }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, setSettingsDialogOpen, setSettingsDialogBookKey, setActiveSettingsItemId } =
|
||||
useSettingsStore();
|
||||
const { getView, getProgress, getViewSettings } = useReaderStore();
|
||||
const { getBookData, getConfig, setConfig, saveConfig } = useBookDataStore();
|
||||
const settings = useSettingsStore((s) => s.settings);
|
||||
const setSettingsDialogOpen = useSettingsStore((s) => s.setSettingsDialogOpen);
|
||||
const setSettingsDialogBookKey = useSettingsStore((s) => s.setSettingsDialogBookKey);
|
||||
const setActiveSettingsItemId = useSettingsStore((s) => s.setActiveSettingsItemId);
|
||||
const getView = useReaderStore((s) => s.getView);
|
||||
const getProgress = useReaderStore((s) => s.getProgress);
|
||||
const getViewSettings = useReaderStore((s) => s.getViewSettings);
|
||||
const getBookData = useBookDataStore((s) => s.getBookData);
|
||||
const getConfig = useBookDataStore((s) => s.getConfig);
|
||||
const setConfig = useBookDataStore((s) => s.setConfig);
|
||||
const saveConfig = useBookDataStore((s) => s.saveConfig);
|
||||
const { themeCode } = useThemeStore();
|
||||
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
@@ -522,8 +530,9 @@ const RSVPControl: React.FC<RSVPControlProps> = ({ bookKey, gridInsets }) => {
|
||||
await view.renderer.goTo({ index: rsvpSectionRef.current + 1 });
|
||||
}, [bookKey, getProgress, getView, removeRsvpHighlight]);
|
||||
|
||||
// Get current chapter info
|
||||
const progress = getProgress(bookKey);
|
||||
// Get current chapter info — reactive subscription so the RSVP overlay's
|
||||
// chapter pointer follows page turns. Reads from readerProgressStore.
|
||||
const progress = useBookProgress(bookKey);
|
||||
const bookData = getBookData(bookKey);
|
||||
const chapters = bookData?.bookDoc?.toc || [];
|
||||
const currentChapterHref = rsvpChapterHrefRef.current ?? progress?.sectionHref ?? null;
|
||||
|
||||
Reference in New Issue
Block a user