From 4367960fe4be4e3978077af06e63a729d64e32fb Mon Sep 17 00:00:00 2001 From: f0x Date: Mon, 30 Jan 2023 23:46:20 +0100 Subject: [PATCH] checklist performance improvements --- web/source/settings/components/check-list.jsx | 68 +++++++++++----- web/source/settings/lib/form/check-list.jsx | 80 ++++++++++--------- web/source/settings/lib/form/text.jsx | 37 +++++++-- 3 files changed, 120 insertions(+), 65 deletions(-) diff --git a/web/source/settings/components/check-list.jsx b/web/source/settings/components/check-list.jsx index bcf5ce5c4..9692652db 100644 --- a/web/source/settings/components/check-list.jsx +++ b/web/source/settings/components/check-list.jsx @@ -20,31 +20,57 @@ const React = require("react"); -module.exports = function CheckList({ field, Component, header = " All", ...componentProps }) { +module.exports = function CheckList({ field, header = "All", renderEntry }) { + performance.mark("RENDER_CHECKLIST"); return (
- - {Object.values(field.value).map((entry) => ( - field.onChange(entry.key, value)} - entry={entry} - Component={Component} - componentProps={componentProps} - /> - ))} + {header} +
); }; -function CheckListEntry({ entry, onChange, Component, componentProps }) { +function CheckListHeader({ toggleAll, children }) { + return ( + + ); +} + +const CheckListEntries = React.memo(function CheckListEntries({ entries, renderEntry, updateValue }) { + const deferredEntries = React.useDeferredValue(entries); + + return Object.values(deferredEntries).map((entry) => ( + + )); +}); + +/* + React.memo is a performance optimization that only re-renders a CheckListEntry + when it's props actually change, instead of every time anything + in the list (CheckListEntries) updates +*/ +const CheckListEntry = React.memo(function CheckListEntry({ entry, updateValue, renderEntry }) { + const onChange = React.useCallback( + (value) => updateValue(entry.key, value), + [updateValue, entry.key] + ); + return ( ); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/web/source/settings/lib/form/check-list.jsx b/web/source/settings/lib/form/check-list.jsx index 38291ee06..726284c28 100644 --- a/web/source/settings/lib/form/check-list.jsx +++ b/web/source/settings/lib/form/check-list.jsx @@ -22,26 +22,12 @@ const React = require("react"); const syncpipe = require("syncpipe"); const { createSlice } = require("@reduxjs/toolkit"); -const { reducer, actions } = createSlice({ +const slice = createSlice({ name: "checklist", initialState: {}, reducers: { create: (state, { payload }) => { - const { entries, uniqueKey, defaultValue } = payload; - return syncpipe(entries, [ - (_) => _.map((entry) => { - let key = entry[uniqueKey]; - return [ - key, - { - ...entry, - key, - checked: state[key]?.checked ?? entry.checked ?? defaultValue - } - ]; - }), - (_) => Object.fromEntries(_) - ]); + return createState(payload, state); }, updateAll: (state, { payload: value }) => { return syncpipe(state, [ @@ -62,26 +48,40 @@ const { reducer, actions } = createSlice({ } }); +const { reducer: reducer2, actions } = slice; + +function reducer() { + console.log("REDUCING", ...arguments); + return reducer2(...arguments); +} + +function createState({ entries, uniqueKey, defaultValue }, oldState) { + console.log("creating state", oldState); + return syncpipe(entries, [ + (_) => _.map((entry) => { + let key = entry[uniqueKey]; + return [ + key, + { + ...entry, + key, + checked: oldState?.[key]?.checked ?? entry.checked ?? defaultValue + } + ]; + }), + (_) => Object.fromEntries(_) + ]); +} + module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", defaultValue = false }) { - const [state, dispatch] = React.useReducer(reducer, {}); + const [state, dispatch] = React.useReducer(reducer, null, () => createState({ entries, uniqueKey, defaultValue })); const [someSelected, setSomeSelected] = React.useState(false); const [toggleAllState, setToggleAllState] = React.useState(0); const toggleAllRef = React.useRef(null); React.useEffect(() => { - /* - entries changed, update state, - re-using old state if available for key - */ - dispatch(actions.create({ entries, uniqueKey, defaultValue })); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [entries]); - - console.log(state); - - React.useEffect(() => { + performance.mark("GoToSocial-useCheckListInput-useEffect-start"); /* Updates (un)check all checkbox, based on shortcode checkboxes Can be 0 (not checked), 1 (checked) or 2 (indeterminate) */ @@ -108,8 +108,20 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke setToggleAllState(all ? 1 : 0); toggleAllRef.current.indeterminate = false; } + performance.mark("GoToSocial-useCheckListInput-useEffect-finish"); + performance.measure("GoToSocial-useCheckListInput-useEffect-processed", "GoToSocial-useCheckListInput-useEffect-start", "GoToSocial-useCheckListInput-useEffect-finish"); }, [state, toggleAllRef]); + const reset = React.useCallback( + () => dispatch(actions.updateAll(defaultValue)), + [defaultValue] + ); + + const onChange = React.useCallback( + (key, value) => dispatch(actions.update({ key, value })), + [] + ); + return React.useMemo(() => { function toggleAll(e) { let selectAll = e.target.checked; @@ -122,14 +134,6 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke setToggleAllState(selectAll); } - function reset() { - dispatch(actions.updateAll(defaultValue)); - } - - function onChange(key, value) { - dispatch(actions.update({ key, value })); - } - function selectedValues() { return syncpipe(state, [ (_) => Object.values(_), @@ -154,5 +158,5 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke onChange: toggleAll } }); - }, [defaultValue, name, someSelected, state, toggleAllState]); + }, [name, onChange, reset, someSelected, state, toggleAllState]); }; \ No newline at end of file diff --git a/web/source/settings/lib/form/text.jsx b/web/source/settings/lib/form/text.jsx index 70e61657c..dde42d6f1 100644 --- a/web/source/settings/lib/form/text.jsx +++ b/web/source/settings/lib/form/text.jsx @@ -20,14 +20,41 @@ const React = require("react"); -module.exports = function useTextInput({ name, Name }, { validator, defaultValue = "", dontReset = false } = {}) { +module.exports = function useTextInput({ name, Name }, { + defaultValue = "", + dontReset = false, + validator, + initValidation: _initValidation +} = {}) { + const [text, setText] = React.useState(defaultValue); - const [valid, setValid] = React.useState(true); const textRef = React.useRef(null); + const initValidation = React.useRef(_initValidation); // memoize forever + const [validation, setValidation] = React.useState(_initValidation ?? ""); + const [_isValidating, startValidation] = React.useTransition(); + const valid = validation == ""; + + const isFirstUpdate = React.useRef(true); + function onChange(e) { let input = e.target.value; setText(input); + + if (validator) { + startValidation(() => { + let validatorMsg = (isFirstUpdate.current && initValidation.current) + ? initValidation.current + : validator(input); + + if (isFirstUpdate.current) { + isFirstUpdate.current = false; + return; // No need to update anything + } + + setValidation(validatorMsg); + }); + } } function reset() { @@ -38,11 +65,9 @@ module.exports = function useTextInput({ name, Name }, { validator, defaultValue React.useEffect(() => { if (validator && textRef.current) { - let res = validator(text); - setValid(res == ""); - textRef.current.setCustomValidity(res); + textRef.current.setCustomValidity(validation); } - }, [text, textRef, validator]); + }, [validation, validator]); // Array / Object hybrid, for easier access in different contexts return Object.assign([