perf(chat): debounce persisted writes and re-render only the streaming message
- the Zustand persist middleware serialized every saved session and wrote to AsyncStorage on each streamed token (appendToMessage -> set); writes are now debounced (400 ms) and flushed when the app leaves the foreground - every token re-rendered every MessageBubble, re-parsing the Markdown of the whole conversation; rows are memoized so only the changed message renders (covered by MessageList.test.tsx)
This commit is contained in:
1 parent
200fb6afd7
commit
0e5b38fd15
6 files changed
+252
-13
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { AppState } from 'react-native';
|
||||
import type { StateStorage } from 'zustand/middleware';
|
||||
|
||||
export interface DebouncedStorage extends StateStorage {
|
||||
/** Writes every pending value right away. */
|
||||
flush: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a storage so bursts of writes collapse into one trailing write.
|
||||
*
|
||||
* The chat store is persisted on every state change and a streamed answer
|
||||
* changes it once per token, which would serialize every saved session and hit
|
||||
* AsyncStorage dozens of times per second.
|
||||
*/
|
||||
export function createDebouncedStorage(base: StateStorage, delayMs = 400): DebouncedStorage {
|
||||
const pending = new Map<string, string>();
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const flush = async () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
const entries = [...pending.entries()];
|
||||
pending.clear();
|
||||
await Promise.all(entries.map(([name, value]) => base.setItem(name, value)));
|
||||
};
|
||||
|
||||
return {
|
||||
getItem: (name) => {
|
||||
const value = pending.get(name);
|
||||
return value === undefined ? base.getItem(name) : Promise.resolve(value);
|
||||
},
|
||||
setItem: (name, value) => {
|
||||
pending.set(name, value);
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(flush, delayMs);
|
||||
},
|
||||
removeItem: (name) => {
|
||||
pending.delete(name);
|
||||
return base.removeItem(name);
|
||||
},
|
||||
flush,
|
||||
};
|
||||
}
|
||||
|
||||
export const appStorage = createDebouncedStorage(AsyncStorage);
|
||||
|
||||
// Do not lose the last tokens when the app is sent to the background or closed.
|
||||
AppState.addEventListener('change', (state) => {
|
||||
if (state !== 'active') void appStorage.flush();
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
import { DEFAULT_SERVER_CONFIG, LMModel, ServerConfig } from '../services/types';
|
||||
import { appStorage } from './debouncedStorage';
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
@@ -164,7 +164,7 @@ export const useAppStore = create<AppState>()(
|
||||
}),
|
||||
{
|
||||
name: 'localaiapp-storage',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
storage: createJSONStorage(() => appStorage),
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
serverConfig: state.serverConfig,
|
||||
|
||||
Reference in new issue
Block a user