React异步加载大数据致Lighthouse性能评分偏低问题排查与优化
问题描述
我们网站在异步useEffect钩子中加载大量包含Base64格式展览图片的展览数据,尽管已经使用lazy和Suspense进行组件懒加载,但Google Lighthouse性能评分依然不理想。想明确该情况导致低分的原因,并获取可行的优化方案。相关代码示例如下:
MainPage组件代码
import React, { Component, Fragment } from 'react'; ... const MeetUs = React.lazy(() => import('./MeetUs.container.js')); function MainPage(props) { return ( <> ... <React.Suspense fallback={<div></div>}> <MeetUs key="meetUs" currentLanguage={props.currentLanguage} /> </React.Suspense> </> ); } export default MainPage;
MeetUs组件代码
import React, { Component, useState, useEffect } from 'react'; ... const Exhibition = React.lazy(() => import('./Exhibition.component')); ... function MeetUs(props) { const [exhibitions, setExhibitions] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { async function loadExhibitions() { return new Promise((resolve, reject) => { window.axios.post(window.location.origin + '/company/exhibitions', { requestType: 'getExhibitions' }) .then((response) => { if(response.data?.success) { resolve(response.data.exhibitions); } else { reject({error: 'Not successful'}); } }) .catch(function (error) { console.log(error); reject(error); }); }); } loadExhibitions() .then((exhibitions) => { setExhibitions(exhibitions); }) .then(() => { setIsLoading(false); }) .catch((error) => { setIsLoading(false); }); },[]); return ( <div className="container-fluid marginTop50px"> ... { exhibitions.map((exhibition, index) => { return ( <React.Suspense fallback={<div></div>}> <React.Fragment> <ExhibitionDiv> <Exhibition/> </ExhibitionDiv> </React.Fragment> </React.Suspense> ) }) } ... </div> ); } export default MeetUs;
低分原因分析
- 组件懒加载不覆盖数据加载:
React.lazy仅负责拆分组件代码包,不会处理数据加载逻辑。当前数据请求在MeetUs组件挂载后立即通过useEffect发起,大体积的Base64数据会阻塞主线程,直接拉低First Contentful Paint (FCP)、Largest Contentful Paint (LCP)等核心性能指标。 - Base64图片体积冗余:Base64编码会使图片体积比原文件增加约30%,大量这类数据一次性加载会拉长网络请求耗时,同时解析Base64的过程也会占用主线程资源,延缓页面渲染。
- 无效的Suspense嵌套:在列表循环中给每个
Exhibition组件单独套Suspense,既没有关联数据加载逻辑,又增加了React的渲染开销;且fallback为空div,无法提供有效加载状态,也会影响用户体验维度的评分。 - 数据加载时机不合理:组件挂载时正处于浏览器关键渲染阶段,此时发起大体积数据请求,会抢占主线程资源,导致页面渲染延迟。
优化方案
1. 替换Base64为图片URL
后端直接返回图片的服务器/CDN地址,而非Base64编码。这样浏览器可单独缓存图片、并行加载,还能通过loading="lazy"实现图片懒加载,大幅减少初始数据体积。
- 接口返回示例:
{ "imageUrl": "/assets/exhibitions/2024-spring.jpg", ... } - 组件使用示例:
// Exhibition组件内 <img src={props.data.imageUrl} loading="lazy" alt="展览现场" />
2. 数据分页或虚拟滚动
避免一次性加载所有展览数据,采用分页加载或虚拟滚动方案:
- 分页加载:给接口添加
page和limit参数,在MeetUs中维护页码状态,监听滚动事件触发下一页请求; - 虚拟滚动:使用
react-window或react-virtualized库,仅渲染当前视口内的展览项,减少DOM节点数量和初始数据量。
3. 数据预加载与缓存
- 若采用SSR/SSG架构(如Next.js),将数据请求移至服务端(
getServerSideProps/getStaticProps),提前注入组件数据,避免客户端加载阻塞; - 纯客户端场景下,用
localStorage/sessionStorage缓存已加载的展览数据,用户再次访问时直接读取缓存,减少重复请求。
4. 用Suspense处理数据加载(React 18+)
React 18支持用Suspense处理异步数据,可借助react-query等库实现,替代useEffect的异步请求逻辑,避免状态更新导致的重复渲染:
import { useQuery } from '@tanstack/react-query'; function MeetUs(props) { const { data: exhibitions, isLoading } = useQuery({ queryKey: ['exhibitions'], queryFn: () => window.axios.post('/company/exhibitions', { requestType: 'getExhibitions' }) .then(res => res.data.exhibitions) }); if (isLoading) return <div>加载展览信息中...</div>; return ( <div className="container-fluid marginTop50px"> {exhibitions.map((item, idx) => ( <ExhibitionDiv key={idx}> <Exhibition data={item} /> </ExhibitionDiv> ))} </div> ); }
5. 优化组件懒加载逻辑
移除列表循环中的嵌套Suspense,将Exhibition的懒加载提升到组件顶部,用单个Suspense包裹所有列表项,减少渲染开销:
const Exhibition = React.lazy(() => import('./Exhibition.component')); function MeetUs(props) { // ...数据逻辑 return ( <div className="container-fluid marginTop50px"> ... <React.Suspense fallback={<div>加载展览项...</div>}> {exhibitions.map((item, idx) => ( <ExhibitionDiv key={idx}> <Exhibition data={item} /> </ExhibitionDiv> ))} </React.Suspense> ... </div> ); }
6. 隔离主线程阻塞(若必须保留Base64)
将Base64解析逻辑移至Web Worker中,避免占用主线程:
// base64-worker.js self.onmessage = (e) => { const [header, data] = e.data.base64.split(','); const mime = header.match(/:(.*?);/)[1]; const byteString = atob(data); const uint8Array = new Uint8Array(byteString.length); for (let i = 0; i < byteString.length; i++) { uint8Array[i] = byteString.charCodeAt(i); } const blob = new Blob([uint8Array], { type: mime }); self.postMessage({ url: URL.createObjectURL(blob) }); }; // MeetUs组件内使用 const handleBase64Image = (base64) => { const worker = new Worker('./base64-worker.js'); worker.postMessage({ base64 }); worker.onmessage = (e) => { // 更新图片URL状态 setImageUrl(e.data.url); worker.terminate(); }; };
内容的提问来源于stack exchange,提问作者dns_nx
相关产品推荐
相关产品推荐

