React Native中Formik多字段表单性能卡顿,如何避免不必要重渲染?
我在React Native中使用Formik时遇到严重性能问题:4个字段时轻微卡顿尚可接受,扩展到12个字段后卡顿明显,点击Checkbox需要3-4秒才响应。排查后发现每次操作都会触发所有字段重渲染。尝试过用FastField优化,但它在React Native里无法正常工作。请问如何优化性能,避免未修改字段的不必要重渲染?
我的每个字段组件包含两部分:
- 控制输入框禁用状态的Checkbox
- 可通过键盘或按钮输入数值的输入框
代码示例
<Formik innerRef={formRef} initialValues={updatedValues} validationSchema={validationSchema} onSubmit={() => {}}> <Box> <CheckboxWithInputControls checkboxFieldName={'addedPipeline'} inputFieldName={'pipeline'} label={t('additionals.pipeline')} step={amount} /> <Divider /> <CheckboxWithInputControls checkboxFieldName={'addedHose'} inputFieldName={'hose'} label={t('additionals.hose')} step={amount} /> ..... <FormChangesListener setIsValid={setIsValid} setValues={setValues} /> </Box> </Formik> export const CheckboxWithInputControls = ({ checkboxFieldName, inputFieldName, label, step, inputRightElement }: CheckboxWithInputControlsProps) => { const [{ value: isChecked }, _meta, { setValue }] = useField(checkboxFieldName) return ( <Box mb="4"> <Box my="2" p="2" bg={isChecked ? 'blue.100' : 'white'} borderRadius="md"> <Checkbox value={checkboxFieldName} isChecked={isChecked} onChange={() => setValue(!isChecked, false)} > {label} </Checkbox> </Box> <Box pl="10"> <NumberInputWithControls controlName={inputFieldName} disabled={!isChecked} step={step} inputRightElement={inputRightElement} /> </Box> </Box> ) } const FormChangesListener =({ setIsValid, setValues }:AdditionsFormProps) => { const { values, isValid } = useFormikContext() useEffect(() => { setIsValid(isValid) setValues(values as FormProps) // eslint-disable-next-line react-hooks/exhaustive-deps }, [values, isValid]) return null }
Why-do-you-render检测结果
GROUP CheckboxWithInputControls LOG {"CheckboxWithInputControls": [Function CheckboxWithInputControls]} Re-rendered because of hook changes: GROUP [hook useContext result] LOG different objects that are equal by value.
临时解决方案(不满意)
放弃在CheckboxWithInputControls中使用useField,改为通过props传递values等数据,此时仅修改的字段会重渲染,性能恢复正常。代码示例:
({ values, setFieldValue, touched, errors }) => ( <Box> <CheckboxWithInputControls checkboxFieldName={'addedPipeline'} inputFieldName={'pipeline'} label={t('additionals.pipeline')} step={amount} value={values.addedPipeline} subValue={values.pipeline} setFieldValue={setFieldValue} touched={touched.pipeline} error={errors.pipeline} /> )})
1. 用React.memo包裹字段组件
将CheckboxWithInputControls和NumberInputWithControls用React.memo包裹,避免父组件重渲染时触发无意义的子组件更新。注意确保组件接收的props是可浅比较的(比如避免传递匿名函数、每次渲染生成新对象的props)。
修改后的CheckboxWithInputControls示例:
export const CheckboxWithInputControls = React.memo(({ checkboxFieldName, inputFieldName, label, step, inputRightElement, value, subValue, setFieldValue, touched, error }: CheckboxWithInputControlsProps) => { // 原有组件逻辑保持不变 })
2. 精准订阅Formik上下文,替代useField
useField会订阅整个Formik上下文的变化,导致全表单重渲染。改用useFormikContext并仅提取当前组件需要的字段值,结合useCallback缓存事件处理函数:
export const CheckboxWithInputControls = React.memo(({ checkboxFieldName, inputFieldName, label, step, inputRightElement }: CheckboxWithInputControlsProps) => { const { values, setFieldValue, touched, errors } = useFormikContext() // 只提取当前组件依赖的字段,避免订阅整个values对象 const isChecked = values[checkboxFieldName] const inputValue = values[inputFieldName] const inputTouched = touched[inputFieldName] const inputError = errors[inputFieldName] // 缓存Checkbox的change事件,避免每次渲染生成新函数 const handleCheckboxToggle = useCallback(() => { setFieldValue(checkboxFieldName, !isChecked, false) }, [checkboxFieldName, isChecked, setFieldValue]) return ( <Box mb="4"> <Box my="2" p="2" bg={isChecked ? 'blue.100' : 'white'} borderRadius="md"> <Checkbox value={checkboxFieldName} isChecked={isChecked} onChange={handleCheckboxToggle} > {label} </Checkbox> </Box> <Box pl="10"> <NumberInputWithControls controlName={inputFieldName} disabled={!isChecked} step={step} inputRightElement={inputRightElement} value={inputValue} touched={inputTouched} error={inputError} setFieldValue={setFieldValue} /> </Box> </Box> ) })
3. 优化FormChangesListener的订阅逻辑
当前组件订阅了整个values对象,任何字段变化都会触发外部状态更新。可以通过useMemo对values进行深比较缓存,避免浅比较导致的不必要更新:
const FormChangesListener = ({ setIsValid, setValues }: AdditionsFormProps) => { const { values, isValid } = useFormikContext() // 对values进行深比较缓存,避免嵌套对象变化时的频繁更新 const memoizedValues = useMemo(() => values, [JSON.stringify(values)]) useEffect(() => { setIsValid(isValid) setValues(memoizedValues as FormProps) }, [memoizedValues, isValid, setIsValid, setValues]) return null }
4. 减少不必要的表单验证触发
在调用setFieldValue时保持第三个参数为false(你已经在使用),跳过实时验证。如果业务允许,可以只在提交前执行完整验证,进一步减少重渲染次数。
内容的提问来源于stack exchange,提问作者Jacki

