DOM 操作优化与缓存管理

性能优化 - React.memo、API并行化、useReducer重构
This commit is contained in:
zdl
2026-01-15 17:50:05 +08:00
parent 6cf9dca324
commit afdc94049c
4 changed files with 74 additions and 38 deletions

View File

@@ -18,15 +18,10 @@ import { Box, Text, VStack, Tooltip } from "@chakra-ui/react";
import { keyframes } from "@emotion/react"; import { keyframes } from "@emotion/react";
import dayjs from "dayjs"; import dayjs from "dayjs";
// 动画定义 // 动画定义 - 使用 transform 代替 background-positionGPU 加速)
const shimmer = keyframes` const shimmer = keyframes`
0% { background-position: -200% 0; } 0% { transform: translateX(-100%); }
100% { background-position: 200% 0; } 100% { transform: translateX(100%); }
`;
const glow = keyframes`
0%, 100% { box-shadow: 0 0 5px rgba(212, 175, 55, 0.3); }
50% { box-shadow: 0 0 20px rgba(212, 175, 55, 0.6); }
`; `;
/** /**
@@ -408,12 +403,9 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
const dateStr = dayjs(date).format("YYYYMMDD"); const dateStr = dayjs(date).format("YYYYMMDD");
const dateData = dataMapRef.current.get(dateStr); const dateData = dataMapRef.current.get(dateStr);
// 找到 day-top 容器并插入自定义内容 // 找到 day-top 容器并插入自定义内容(直接替换,无需先清空)
const dayTop = el.querySelector(".fc-daygrid-day-top"); const dayTop = el.querySelector(".fc-daygrid-day-top");
if (dayTop) { if (dayTop) {
// 清空默认内容
dayTop.innerHTML = "";
// 插入自定义内容
dayTop.innerHTML = createCellContentHTML(date, dateData, isToday); dayTop.innerHTML = createCellContentHTML(date, dateData, isToday);
} }
}, []); }, []);
@@ -463,17 +455,17 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
overflow="hidden" overflow="hidden"
position="relative" position="relative"
> >
{/* 闪光效果 */} {/* 闪光效果 - 使用 transform 实现 GPU 加速 */}
<Box <Box
position="absolute" position="absolute"
top="0" top="0"
left="0" left="0"
right="0" w="100%"
bottom="0" h="100%"
bgGradient="linear(to-r, transparent, rgba(255,255,255,0.3), transparent)" bgGradient="linear(to-r, transparent, rgba(255,255,255,0.4), transparent)"
backgroundSize="200% 100%" animation={`${shimmer} 2.5s ease-in-out infinite`}
animation={`${shimmer} 3s linear infinite`} opacity={0.6}
opacity={0.5} pointerEvents="none"
/> />
<Text <Text
fontSize="xs" fontSize="xs"
@@ -579,7 +571,8 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
}, },
".fc-daygrid-day.fc-day-today": { ".fc-daygrid-day.fc-day-today": {
bg: "rgba(212, 175, 55, 0.15) !important", bg: "rgba(212, 175, 55, 0.15) !important",
animation: `${glow} 2s ease-in-out infinite`, boxShadow: "0 0 15px rgba(212, 175, 55, 0.5)",
// 移除呼吸动画,使用固定 boxShadow 高亮"今天",避免内容闪烁
}, },
".fc-daygrid-day-frame": { ".fc-daygrid-day-frame": {
minHeight: "50px", minHeight: "50px",

View File

@@ -1,5 +1,5 @@
// HeroPanel - 综合日历组件 // HeroPanel - 综合日历组件
import React, { useState, useEffect, useCallback, Suspense, lazy, memo } from "react"; import React, { useState, useEffect, useCallback, Suspense, lazy, memo, useRef } from "react";
import { import {
Box, Box,
HStack, HStack,
@@ -43,12 +43,23 @@ const CombinedCalendar = memo(({ DetailModal }) => {
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
// 加载日历综合数据(一次 API 调用获取所有数据 // 月份数据缓存(避免切换月份后再切回时重复请求
const monthCacheRef = useRef({});
// 加载日历综合数据(带缓存)
useEffect(() => { useEffect(() => {
const loadCalendarCombinedData = async () => { const loadCalendarCombinedData = async () => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
const cacheKey = `${year}-${month}`;
// 检查缓存
if (monthCacheRef.current[cacheKey]) {
setCalendarData(monthCacheRef.current[cacheKey]);
return;
}
try { try {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
const response = await fetch( const response = await fetch(
`${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}` `${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}`
); );
@@ -63,10 +74,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
eventCount: item.event_count || 0, eventCount: item.event_count || 0,
indexChange: item.index_change, indexChange: item.index_change,
})); }));
console.log( // 存入缓存
"[HeroPanel] 加载日历综合数据成功,数据条数:", monthCacheRef.current[cacheKey] = formattedData;
formattedData.length
);
setCalendarData(formattedData); setCalendarData(formattedData);
} }
} }

View File

@@ -268,14 +268,23 @@ const DetailModal = ({
[dispatch, isStockInWatchlist] [dispatch, isStockInWatchlist]
); );
// 加载股票行情(并行加载优化 // 加载股票行情(并行加载 + 缓存去重
const loadStockQuotes = useCallback( const loadStockQuotes = useCallback(
async (stocks) => { async (stocks) => {
if (!stocks || stocks.length === 0) return; if (!stocks || stocks.length === 0) return;
// 过滤已缓存的股票,只请求未缓存的
const uncachedStocks = stocks.filter(
(stock) => !stockQuotes[stock.code]
);
// 如果全部已缓存,无需请求
if (uncachedStocks.length === 0) return;
setStockQuotesLoading(true); setStockQuotesLoading(true);
// 并行发起所有请求 // 并行发起未缓存股票的请求
const promises = stocks.map(async (stock) => { const promises = uncachedStocks.map(async (stock) => {
const code = getSixDigitCode(stock.code); const code = getSixDigitCode(stock.code);
try { try {
const response = await fetch( const response = await fetch(
@@ -304,18 +313,18 @@ const DetailModal = ({
// 等待所有请求完成 // 等待所有请求完成
const results = await Promise.all(promises); const results = await Promise.all(promises);
// 构建 quotes 对象 // 合并新数据到现有缓存
const quotes = {}; const newQuotes = { ...stockQuotes };
results.forEach((result) => { results.forEach((result) => {
if (result) { if (result) {
quotes[result.stockCode] = result.quote; newQuotes[result.stockCode] = result.quote;
} }
}); });
setStockQuotes(quotes); setStockQuotes(newQuotes);
setStockQuotesLoading(false); setStockQuotesLoading(false);
}, },
[setStockQuotes, setStockQuotesLoading] [stockQuotes, setStockQuotes, setStockQuotesLoading]
); );
// 显示相关股票 // 显示相关股票

View File

@@ -4,7 +4,7 @@
* Y轴板块热度涨停家数 * Y轴板块热度涨停家数
* 支持时间滑动条查看历史数据 * 支持时间滑动条查看历史数据
*/ */
import React, { useState, useEffect, useMemo, useCallback } from "react"; import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { import {
Box, Box,
Text, Text,
@@ -38,6 +38,9 @@ import {
/** /**
* ThemeCometChart 主组件 * ThemeCometChart 主组件
*/ */
// 缓存有效期5 分钟)
const CACHE_DURATION = 5 * 60 * 1000;
const ThemeCometChart = ({ onThemeSelect }) => { const ThemeCometChart = ({ onThemeSelect }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [allDatesData, setAllDatesData] = useState({}); const [allDatesData, setAllDatesData] = useState({});
@@ -48,8 +51,24 @@ const ThemeCometChart = ({ onThemeSelect }) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast(); const toast = useToast();
// 加载所有日期的数据 // 数据缓存(避免 tab 切换时重复请求)
const dataCacheRef = useRef({ data: null, dates: null, timestamp: null });
// 加载所有日期的数据(带缓存)
const loadAllData = useCallback(async () => { const loadAllData = useCallback(async () => {
// 检查缓存是否有效5分钟内
const now = Date.now();
if (
dataCacheRef.current.timestamp &&
now - dataCacheRef.current.timestamp < CACHE_DURATION &&
dataCacheRef.current.data
) {
setAllDatesData(dataCacheRef.current.data);
setAvailableDates(dataCacheRef.current.dates);
setLoading(false);
return;
}
setLoading(true); setLoading(true);
try { try {
const apiBase = getApiBase(); const apiBase = getApiBase();
@@ -109,6 +128,12 @@ const ThemeCometChart = ({ onThemeSelect }) => {
} }
}); });
// 存入缓存
dataCacheRef.current = {
data: dataCache,
dates: dates,
timestamp: Date.now(),
};
setAllDatesData(dataCache); setAllDatesData(dataCache);
setSliderIndex(0); setSliderIndex(0);
} else { } else {