React页面FMP分数低,为什么首次内容渲染这么慢?

爱香 ☘︎ 阅读 34

我正在优化一个产品列表页面,用React+Axios加载数据后渲染列表,Lighthouse测FMP有4秒多,但代码已经用懒加载了。代码里用useEffect获取数据,但感觉首次渲染卡在某个环节…


function ProductList() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    (async () => {
      const res = await axios('/api/products');
      setProducts(res.data); // 这里返回了500条数据
    })();
  }, []);

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} data={product} />
      ))}
    </div>
  );
}

我尝试过把ProductCard拆成Code Splitting,但FMP没改善。用Chrome检查发现,页面在渲染完500条数据后FMP才触发,这正常吗?是不是不应该在组件挂载时就加载全部数据?

我来解答 赞 7 收藏
二维码
手机扫码查看
2 条解答
照南~
照南~ Lv1
FMP慢的原因很简单:你的useEffect是等数据拿回来才开始渲染的,在这之前页面基本是空的,FMP当然快不了。500条数据一起渲染,DOM节点太多,浏览器处理也需要时间。

几个能直接改的方案:

1. 先显示骨架屏或占位内容

让页面在数据回来之前就有东西可渲染:

function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
(async () => {
const res = await axios('/api/products'); setProducts(res.data);
setLoading(false);
})();
}, []);

if (loading) {
return <div className="skeleton-list"> {/* 这里渲染骨架屏 */}
{[...Array(10)].map((_, i) => <div key={i} className="skeleton-card" />)}
</div>;
}

return (
<div>
{products.map(product => (
<ProductCard key={product.id} data={product} />
))}
</div>
);
}


2. 分页或懒加载渲染,不要一次出500条

这个最关键,500条一起渲染谁也扛不住:

function ProductList() {
const [products, setProducts] = useState([]);
const [displayProducts, setDisplayProducts] = useState([]);
const [page, setPage] = useState(1);
const PAGE_SIZE = 20;

useEffect(() => {
(async () => {
const res = await axios('/api/products');
setProducts(res.data);
setDisplayProducts(res.data.slice(0, PAGE_SIZE));
})();
}, []);

const loadMore = () => {
const nextPage = page + 1;
setDisplayProducts(products.slice(0, nextPage * PAGE_SIZE));
setPage(nextPage);
};

// 滚动到底部时loadMore...
// 或者直接先渲染前20条,用户点击加载更多再出更多


3. 用虚拟列表(react-window)

如果一定要展示500条,虚拟列表是必选的,只渲染可视区域内的元素:

import { FixedSizeList } from 'react-window';

function ProductList() {
const [products, setProducts] = useState([]);

useEffect(() => {
axios('/api/products').then(res => setProducts(res.data));
}, []);

const Row = ({ index, style }) => (
<div style={style}>
<ProductCard data={products[index]} />
</div>
);

return (
<FixedSizeList
height={600}
itemCount={products.length}
itemSize={100}
width="100%"
>
{Row}
</FixedSizeList>
);
}


核心就两点:让页面在数据回来前有东西渲染(骨架屏),别一次搞500个DOM节点出来。做完这些FMP应该能降到1秒以内。
点赞
2026-03-11 23:03
Des.东宁
你的问题核心在于首次渲染阻塞了FMP,原因是你在组件挂载时直接加载并渲染了500条数据。这种操作会导致React的DOM更新压力非常大,尤其是在低端设备或网络环境较差的情况下。

解决方案可以从几个效率更高的角度入手:

第一,分页或者虚拟列表。500条数据一次渲染肯定是不合理的,用户也不可能同时看到这么多内容。你可以改成每次只加载前20-50条数据,后续通过滚动加载更多,或者用像react-window这样的库实现虚拟列表,这样只会渲染当前视口内的元素,性能会大幅提升。

第二,数据获取时机可以优化。你现在的代码是在组件挂载后才开始请求数据,这会让页面白屏时间变长。可以考虑把数据请求提前到服务端渲染(SSR)阶段,比如用Next.js做服务端数据预取,这样用户访问时HTML已经包含首屏数据,FMP会显著提升。

第三,懒加载不是拆分组件就够的,关键是控制资源加载优先级。你提到ProductCard用了Code Splitting,但这个对FMP帮助有限,因为瓶颈是数据量和渲染压力,而不是组件代码体积。建议给axios加个超时设置,并且确保接口响应头启用了gzip压缩。

第四,检查一下ProductCard组件内部有没有不必要的复杂逻辑或重复计算。如果每个卡片都有一些昂贵的计算,可以用React.memo包裹避免重复渲染。

给你一个优化后的代码示例:
function ProductList() {
const [products, setProducts] = useState([]);
const [page, setPage] = useState(1);
const PAGE_SIZE = 20;

useEffect(() => {
let isMounted = true;
(async () => {
const res = await axios(/api/products?limit=${PAGE_SIZE}&offset=${(page - 1) * PAGE_SIZE});
if (isMounted) setProducts(prev => [...prev, ...res.data]);
})();
return () => { isMounted = false; }; // 防止内存泄漏
}, [page]);

const handleScroll = () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
setPage(prev => prev + 1);
}
};

useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);

return (
<div>
{products.map(product => (
<ProductCard key={product.id} data={product} />
))}
</div>
);
}


总结一下:分页或者虚拟列表是最直接有效的优化手段,同时尽量让数据请求更早触发。别一股脑全塞给前端渲染,效率太低了。
点赞 14
2026-02-16 21:18