You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React搜索栏组件优化:解决渲染延迟与内存泄漏问题

React搜索组件优化方案:解决延迟、白屏与内存泄漏问题

问题根源拆解

  1. 请求频率过高:输入每个字符都会触发useEffect执行API请求,短时间内大量请求导致页面卡顿甚至白屏
  2. 内存泄漏:异步请求未取消,组件卸载后请求仍在进行,完成后尝试更新已卸载组件的状态
  3. 逻辑错误:调用searchActors时直接使用初始状态的newMovieSearch[1].SID,此时电影搜索结果还未返回,会触发Cannot read property 'SID' of undefined错误
  4. loading状态失效:setSearch(false)在请求发起后立即执行,导致加载提示根本不会显示

具体优化步骤

1. 添加防抖(Debounce)

防抖可以让API请求在用户停止输入一段时间后才触发,避免频繁请求。这里手动实现简单防抖逻辑:

const Home: React.FC = () => {
  // ... 其他状态
  const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedSearchTerm(searchTerm);
    }, 300); // 300ms内无输入才触发搜索

    return () => clearTimeout(timer);
  }, [searchTerm]);
}

后续所有API请求都基于debouncedSearchTerm触发,而非原始的searchTerm。

2. 使用AbortController取消异步请求

在组件卸载或搜索词变化时,终止未完成的请求,避免内存泄漏:

useEffect(() => {
  if (!debouncedSearchTerm) {
    setMovieSearch([]);
    setActorSearch([]);
    return;
  }

  const controller = new AbortController();
  const signal = controller.signal;

  setSearch(true);

  // 先请求电影,再基于电影结果请求演员
  searchMovies(debouncedSearchTerm, { signal })
    .then(movieResults => {
      setMovieSearch(movieResults);
      // 确保有电影结果再请求演员
      if (movieResults.length >= 2) {
        return searchActors(debouncedSearchTerm, movieResults[1].SID, { signal });
      }
      return [];
    })
    .then(actorResults => {
      setActorSearch(actorResults);
    })
    .catch(err => {
      // 忽略取消请求的错误
      if (err.name !== 'AbortError') console.error(err);
    })
    .finally(() => {
      setSearch(false);
    });

  // 清理函数:取消请求
  return () => controller.abort();
}, [debouncedSearchTerm]);

注意需要修改searchMovies和searchActors函数,支持接收signal参数传递给fetch:

// 示例API函数修改
export const searchMovies = (term: string, { signal }: { signal: AbortSignal }) => {
  return fetch(`/api/movies?search=${term}`, { signal })
    .then(res => res.json())
    .then(data => data as MovieAttributes[]);
};

export const searchActors = (term: string, sid: string, { signal }: { signal: AbortSignal }) => {
  return fetch(`/api/actors?search=${term}&sid=${sid}`, { signal })
    .then(res => res.json())
    .then(data => data as ActorAttributes[]);
};

3. 修复异步依赖逻辑

之前的代码在电影请求还未返回时就尝试读取newMovieSearch[1].SID,这完全是错误的。必须等电影请求完成后,再根据返回的结果调用演员搜索,也就是上面代码中searchMovies的.then里处理演员请求的逻辑。

4. 优化列表渲染性能

不要在map里做过滤逻辑,提前在状态更新时处理,或者直接基于搜索结果渲染(因为API已经返回过滤后的结果):

// 替换原来的列表渲染代码
<div className="App">
  <ul className="posts">
    <input type="text" value={searchTerm} onChange={handleSearchTermChange} />
    {movieSearch.map((movie) => (
      <li key={movie.OBJECTID}>
        <h3>{movie.NAME}</h3>
        <p>{movie.DIRECTOR}</p>
        <p>{movie.DESCRIP}</p>
      </li>
    ))}
  </ul>
</div>

如果API没有做过滤,也可以在searchMovies返回后直接过滤,避免每次渲染都执行toLowerCase()和includes():

searchMovies(debouncedSearchTerm, { signal })
  .then(rawResults => {
    const filteredMovies = rawResults.filter(movie => 
      movie.NAME.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
    );
    setMovieSearch(filteredMovies);
    // ... 后续逻辑
  })

5. 边界情况处理

  • 当搜索词为空时,清空搜索结果
  • 处理电影结果不足2条的情况,避免读取movieResults[1]时出错

完整优化后代码示例

import { useEffect, useState } from 'react';
import { searchMovies, searchActors, MovieAttributes, ActorAttributes } from "@utils/atts"

const Home: React.FC = () => {
    const [search, setSearch] = useState(false);
    const [movieSearch, setMovieSearch] = useState<MovieAttributes[]>([]);
    const [actorSearch, setActorSearch] = useState<ActorAttributes[]>([]);
    const [searchTerm, setSearchTerm] = useState('');
    const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');

    // 防抖处理搜索词
    useEffect(() => {
        const timer = setTimeout(() => {
            setDebouncedSearchTerm(searchTerm);
        }, 300);

        return () => clearTimeout(timer);
    }, [searchTerm]);

    // 处理搜索请求
    useEffect(() => {
        // 搜索词为空时清空结果
        if (!debouncedSearchTerm) {
            setMovieSearch([]);
            setActorSearch([]);
            return;
        }

        const controller = new AbortController();
        const signal = controller.signal;

        setSearch(true);

        searchMovies(debouncedSearchTerm, { signal })
            .then(movieResults => {
                setMovieSearch(movieResults);
                // 确保有足够的电影结果再请求演员
                if (movieResults.length >= 2) {
                    return searchActors(debouncedSearchTerm, movieResults[1].SID, { signal });
                }
                return [];
            })
            .then(actorResults => {
                setActorSearch(actorResults);
            })
            .catch(err => {
                if (err.name !== 'AbortError') {
                    console.error('搜索出错:', err);
                }
            })
            .finally(() => {
                setSearch(false);
            });

        // 清理函数:取消未完成的请求
        return () => controller.abort();
    }, [debouncedSearchTerm]);

    const handleSearchTermChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setSearchTerm(event.target.value);
    }

    return (
        <div>
            <input type="text" value={searchTerm} onChange={handleSearchTermChange} />
            {search && <p>搜索中...</p>}
            {movieSearch.length > 0 && <p>找到 {movieSearch.length} 部电影</p>}
            {actorSearch.length > 0 && <p>找到 {actorSearch.length} 位演员</p>}
            
            <div className="App">
                <ul className="posts">
                    {movieSearch.map((movie) => (
                        <li key={movie.OBJECTID}>
                            <h3>{movie.NAME}</h3>
                            <p>{movie.DIRECTOR}</p>
                            <p>{movie.DESCRIP}</p>
                        </li>
                    ))}
                </ul>
            </div>
        </div>
    );
}

export default Home;

内容的提问来源于stack exchange,提问作者peachy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 20:20:35