React实时健身搜索API优化及重渲染问题求助
健身训练搜索功能优化:解决大Payload延迟与过度重渲染问题
一、解决首次请求Payload过大导致的延迟问题
单次请求返回500MB数据完全是不必要的,核心优化思路是减少数据传输量和控制请求频率,具体方案如下:
1. 后端裁剪返回字段
当前Prisma默认返回exercises表的所有字段,很多字段前端根本用不到(比如训练描述、额外元数据),直接导致Payload体积暴涨。修改后端代码,只返回前端需要的字段:
export default async function handler(req, res) { try { const param = req.query.slug[0]; if (!param) return res.status(400).send("缺少搜索关键词"); const exercises = await prisma.exercises.findMany({ where: { OR: [ { name: { contains: param } }, { target: { contains: param } }, { equipment: { contains: param } } ] }, // 只返回前端需要的三个字段 select: { id: true, name: true, gifUrl: true } }); res.status(200).send(exercises); } catch (error) { console.log(error); res.status(500).send(error); } }
2. 实现分页查询
即使裁剪了字段,匹配结果过多时单次返回数据量依然不小,添加分页参数限制单次返回条数,前端滚动到底部再加载下一页:
export default async function handler(req, res) { try { const param = req.query.slug[0]; const page = parseInt(req.query.page) || 1; const pageSize = parseInt(req.query.pageSize) || 20; if (!param) return res.status(400).send("缺少搜索关键词"); // 查询当前页数据 const exercises = await prisma.exercises.findMany({ where: { OR: [ { name: { contains: param } }, { target: { contains: param } }, { equipment: { contains: param } } ] }, select: { id: true, name: true, gifUrl: true }, skip: (page - 1) * pageSize, take: pageSize }); // 返回总条数,方便前端判断是否还有更多数据 const total = await prisma.exercises.count({ where: { OR: [ { name: { contains: param } }, { target: { contains: param } }, { equipment: { contains: param } } ] } }); res.status(200).send({ data: exercises, total }); } catch (error) { console.log(error); res.status(500).send(error); } }
3. 前端添加搜索防抖
每次按键都发请求会导致频繁触发大请求,用防抖函数限制请求频率(比如300ms内只发一次):
import { useState, useCallback, useEffect } from 'react'; // 防抖工具函数 const debounce = (func, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }; export default function ExerciseSearch() { const [keyWord, setKeyWord] = useState(''); const [array, setArray] = useState([]); const [hasMore, setHasMore] = useState(true); const [page, setPage] = useState(1); // 防抖后的搜索函数 const debouncedSearch = useCallback(debounce(async (keyword) => { if (!keyword.trim()) { setArray([]); setPage(1); setHasMore(true); return; } const response = await fetch(`/api/exercises/${encodeURIComponent(keyword)}?page=${1}&pageSize=20`); const { data, total } = await response.json(); setArray(data); setPage(1); setHasMore(data.length < total); }, 300), []); // 监听关键词变化触发防抖搜索 useEffect(() => { debouncedSearch(keyWord); }, [keyWord, debouncedSearch]); // 输入框仅负责更新关键词 const handleInputChange = (event) => { setKeyWord(event.target.value); }; // 滚动加载更多 const loadMore = async () => { const nextPage = page + 1; const response = await fetch(`/api/exercises/${encodeURIComponent(keyWord)}?page=${nextPage}&pageSize=20`); const { data, total } = await response.json(); setArray(prev => [...prev, ...data]); setPage(nextPage); setHasMore(array.length + data.length < total); }; // 滚动监听 const handleScroll = useCallback(() => { const ul = document.querySelector('.exercise-list'); if (!ul || !hasMore) return; if (ul.scrollTop + ul.clientHeight >= ul.scrollHeight) { loadMore(); } }, [hasMore, page]); useEffect(() => { const ul = document.querySelector('.exercise-list'); if (ul) ul.addEventListener('scroll', handleScroll); return () => { if (ul) ul.removeEventListener('scroll', handleScroll); }; }, [handleScroll]); return ( <div className='flex mt-3 flex-col'> <input onChange={handleInputChange} required placeholder='按关键词搜索...' className='border border-slate-400 p-1 rounded-md flex-1 max-w-sm my-2' /> <ul className='exercise-list border border-slate-400 p-1 rounded-md max-w-sm my-2 max-h-52 overflow-scroll'> {array.length > 0 && array.map(exercice => ( <li key={exercice.id} className='flex flex-wrap p-2 bg-slate-200 m-2 items-center rounded-md'> <span><Image className='rounded-xl mr-2' width={40} height={40} src={exercice.gifUrl} alt={exercice.name}/></span> <span>{ exercice.name }</span> </li> ))} </ul> </div> ); }
4. 图片优化
- 移除
priority属性:列表项的GIF不需要优先加载,Next.js的Image组件默认会懒加载,减少初始带宽占用; - 若GIF体积过大,建议后端将其转成WebP格式(体积仅为GIF的1/3左右),前端根据浏览器支持情况选择加载。
二、解决过度重渲染与无限循环问题
1. 修复搜索函数的异步状态问题
原代码中searchExercises里,setKeyWord后立刻用keyWord发请求,但setState是异步的,此时keyWord还是旧值,导致请求关键词错误,同时触发不必要的重渲染。改用上面的防抖方案,通过useEffect监听关键词变化触发请求,避免异步状态问题。
2. 用useCallback包裹点击事件处理函数
直接在map里写onClick={() => setExerciceId(exercice.id)}会导致每次渲染都创建新函数,触发子组件重渲染。用useCallback包裹:
const [exerciceId, setExerciceId] = useState(''); const handleExerciseClick = useCallback((id) => { setExerciceId(id); }, []); // 列表项渲染 <li key={exercice.id} className='flex flex-wrap p-2 bg-slate-200 m-2 items-center rounded-md cursor-pointer' onClick={() => handleExerciseClick(exercice.id)} > <span><Image className='rounded-xl mr-2' width={40} height={40} src={exercice.gifUrl} alt={exercice.name}/></span> <span>{ exercice.name }</span> </li>
3. 修复列表渲染条件
原代码array.length > 1会导致只有2条及以上结果才显示,改成array.length > 0,确保单条结果也能正常展示。
4. 避免不必要的状态更新
请求返回数据后,判断是否与当前array一致,避免重复设置相同状态触发重渲染:
const { data } = await response.json(); if (JSON.stringify(data) !== JSON.stringify(array)) { setArray(data); }
内容的提问来源于stack exchange,提问作者daniel sas
相关产品推荐
相关产品推荐

