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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 02:55:16