← 中文资源

简体中文版本

React — 受控输入每次按键都卡顿

修复 React 受控输入因每次按键重渲染大子树导致的输入迟滞,剖析渲染成本,并收窄状态作用域或防抖持久化。

阅读英文原文

症状

  Profiler 让爆炸半径可见:录制一次按键,火焰图显示这次提交渲染了整页而不只是输入框。“按键到绘制”的路径就是诊断:keystroke → setState → 重渲染 App 加 400 个组件 → paint,其中除输入子树外全是浪费。所有权问题定位根因:value 状态住在哪?住得高且后代昂贵,意味着每个字符都要协调全世界。内联计算检查再分离“渲染浪费”与“计算浪费”:render 体里对上万行的 filter 无论树大小都是每键几十毫秒。迟滞随列表规模与页面复杂度增长,是成本结构的外显。

诊断思路

  先 Profiler 录单键看提交范围与耗时;再定位每次按键变化的状态及其在树中的位置(爆炸半径由位置决定);最后对可疑计算插桩计时,区分渲染树成本与行内计算成本。每次按键同步写服务器或 localStorage 而非防抖,是常被忽略的第三来源。

根因

  受控输入每次按键重渲染其所有者。所有者位于树的高处、下方子树庞大或昂贵时,每个字符都为无关组件付一次完整协调,输入延迟随状态变化波及的一切而增长。

修复与验证

  把输入状态下推到最小所有者:FilterBar 持有 query、memo 化、ResultsTable 不随按键重渲染;昂贵计算 useMemo 化并配合 useDeferredValue 在提交值上过滤;离开组件的一切(服务器、localStorage)防抖 500ms;重编辑器(代码、富文本)用非受控输入加 ref 读取、blur 或显式保存时提交状态,把打字彻底移出渲染路径。验证时万行页面上输入即时:Profiler 每键提交保持在几毫秒且局限于输入子树。盲目把状态从依赖 UI 拆走会造成视图失同步——共享的保持共享,只下推按键局部状态。

代码示例

keystroke -> setState -> re-render App + 400 components -> paint
                                    ^ everything here is waste
function SearchPage({ rows }) {
  return (
    <>
      <FilterBar rows={rows} />   {/* input state lives here */}
      <ResultsTable rows={rows} /> {/* not re-rendered per keystroke */}
    </>
  );
}

const FilterBar = memo(function FilterBar({ rows }) {
  const [query, setQuery] = useState('');
  const deferred = useDeferredValue(query);
  const results = useMemo(
    () => filterRows(rows, deferred),
    [rows, deferred]
  );
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
});
useEffect(() => {
  const t = setTimeout(() => saveDraft(value), 500);
  return () => clearTimeout(t);
}, [value]);