React 18 + .NET 6组件加载优化:两种方案实现与示例
解决方案:组件级渐进式渲染方案
方案一:基于SignalR的组件数据主动推送(优先选择)
核心逻辑
利用SignalR建立后端与前端的实时连接,Controller并行异步获取各组件数据,每完成一个组件的数据抓取,就主动推送给前端对应React组件,组件收到数据后立即渲染,完全无需等待全量数据加载。
实现细节
1. 定义SignalR Hub
public class WidgetDataHub : Hub { // 客户端注册组件监听分组 public async Task RegisterWidget(string widgetGroupId) { await Groups.AddToGroupAsync(Context.ConnectionId, widgetGroupId); } }
2. 拆分数据模型并并行获取
// 组件专属数据契约 public class NameWidgetDataContract { public string FullName { get; set; } public string AvatarUrl { get; set; } } public class TransactionWidgetDataContract { public List<TransactionRecord> RecentTransactions { get; set; } public decimal TotalBalance { get; set; } } // Controller实现 public class AccountController : Controller { private readonly IWidgetDataService _dataService; private readonly IHubContext<WidgetDataHub> _hubContext; public AccountController(IWidgetDataService dataService, IHubContext<WidgetDataHub> hubContext) { _dataService = dataService; _hubContext = hubContext; } public async Task<IActionResult> Index() { // 启动并行任务获取各组件数据 var nameTask = _dataService.GetNameWidgetDataAsync(); var transactionTask = _dataService.GetTransactionWidgetDataAsync(); // 逐个处理完成的任务并推送数据 while (!nameTask.IsCompleted || !transactionTask.IsCompleted) { var completedTask = await Task.WhenAny(nameTask, transactionTask); if (completedTask == nameTask) { var data = await nameTask; await _hubContext.Clients.Group("name-widget").SendAsync("ReceiveWidgetData", data); } else if (completedTask == transactionTask) { var data = await transactionTask; await _hubContext.Clients.Group("transaction-widget").SendAsync("ReceiveWidgetData", data); } } // 返回仅包含组件容器的主页面 return View(); } }
3. 前端React组件实现
// AccountDashboard.jsx import { useEffect, useState } from 'react'; import { HubConnectionBuilder } from '@microsoft/signalr'; import NameWidget from './Name'; import TransactionWidget from './Transaction'; import EmptyStateWidget from './EmptyState'; export default function AccountDashboard() { const [nameData, setNameData] = useState(null); const [transactionData, setTransactionData] = useState(null); useEffect(() => { const connection = new HubConnectionBuilder() .withUrl('/widgetDataHub') .build(); connection.start().then(() => { // 注册组件监听分组 connection.invoke('RegisterWidget', 'name-widget'); connection.invoke('RegisterWidget', 'transaction-widget'); }); // 接收后端推送的组件数据 connection.on('ReceiveWidgetData', (data) => { if (data.fullName) { setNameData(data); } else if (data.recentTransactions) { setTransactionData(data); } }); return () => connection.stop(); }, []); return ( <div className="dashboard-container"> {/* 收到数据立即渲染,否则显示加载态 */} {nameData ? <NameWidget data={nameData} /> : <div className="widget-loader">加载中...</div>} {transactionData ? <TransactionWidget data={transactionData} /> : <div className="widget-loader">加载中...</div>} {/* 无需数据的组件直接渲染 */} <EmptyStateWidget /> </div> ); }
4. 视图页面(Index.cshtml)
@{ ViewData["Title"] = "Account Dashboard"; } <div id="react-dashboard"></div> <script src="~/js/signalr/dist/browser/signalr.js"></script> <script src="~/js/react-bundle.js"></script>
方案优势
- 组件独立渲染:单个组件数据就绪后立即渲染,首屏可见时间大幅缩短
- 彻底规避并发冲突:所有数据由后端主动推送,前端无需发起任何Fetch请求
- 适配React 18特性:结合
useState、useEffect天然支持React 18并发渲染模式 - 扩展性强:新增组件仅需添加对应数据契约和SignalR监听,无需改动整体架构
方案二:全量数据加载前的渐进式占位渲染
核心逻辑
保留原有全量ViewModel传递模式,在React端为每个组件添加加载占位容器,直到全量数据加载完成后统一渲染组件。
实现细节
1. 保留原有ViewModel结构
public class AccountViewModelContract { public NameWidgetDataContract NameWidgetData { get; set; } public TransactionWidgetDataContract TransactionWidgetData { get; set; } }
2. Controller保持全量数据获取逻辑
public async Task<IActionResult> Index() { var viewModel = new AccountViewModelContract { NameWidgetData = await _dataService.GetNameWidgetDataAsync(), TransactionWidgetData = await _dataService.GetTransactionWidgetDataAsync() }; return View(viewModel); }
3. 视图页面序列化全量数据
@{ ViewData["Title"] = "Account Dashboard"; } <input type="hidden" id="dashboard-data" value="@Json.Serialize(Model)" /> <div id="react-dashboard"></div> <script src="~/js/react-bundle.js"></script>
4. React端实现加载态与组件渲染
// AccountDashboard.jsx import { useState, useEffect } from 'react'; import NameWidget from './Name'; import TransactionWidget from './Transaction'; import EmptyStateWidget from './EmptyState'; export default function AccountDashboard() { const [dashboardData, setDashboardData] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const dataElement = document.getElementById('dashboard-data'); if (dataElement) { const data = JSON.parse(dataElement.value); setDashboardData(data); setIsLoading(false); } }, []); return ( <div className="dashboard-container"> {isLoading ? ( {/* 全量加载时显示组件占位骨架 */} <div className="dashboard-skeleton"> <div className="widget-skeleton"></div> <div className="widget-skeleton"></div> <div className="widget-skeleton"></div> </div> ) : ( <> <NameWidget data={dashboardData.NameWidgetData} /> <TransactionWidget data={dashboardData.TransactionWidgetData} /> <EmptyStateWidget /> </> )} </div> ); }
方案优势
- 改动成本极低:无需修改后端核心逻辑,仅需前端添加加载态实现,适合快速迭代
- 数据一致性高:全量数据加载完成后统一渲染,避免组件间数据不一致问题
- 兼容现有架构:完美适配当前未使用SSR Hydration的技术栈,无需引入额外依赖
内容的提问来源于stack exchange,提问作者CompSciFly
相关产品推荐
相关产品推荐

