perf: 今日页首屏 394KB/2.0s → 51.5KB/0.82s
逐个接口计时后的两处改动: - 健康摘要不再下发空值。一天 40 项指标里大部分是这块表没有的传感器, 全按 null 发出去占了约两成体积。客户端本来就把「键不存在」和 null 当同一回事。 - 今日页首屏由 365 天改为 60 天,往前翻越界时再加载 180 天。 一次取一年是为了让翻页不发请求,代价是首屏 394 KB,手机上不划算。 日历选到窗口外的日期同样会自动加载。 顺带把加载逻辑收成一个 loadFrom:原来初始 effect 的依赖是空数组, 日历里改 from 不会触发重新拉取,是个还没被触发的 bug。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, f7 } from 'framework7-react';
|
||||
import { apiClient, errorMessage, HealthDay } from '../services/api';
|
||||
import Screen from '../components/Screen';
|
||||
@@ -12,8 +12,13 @@ import { METRICS, metricHref } from '../lib/metrics';
|
||||
import { daysAgo, iso, shiftDay, today as todayIso } from '../lib/day';
|
||||
import './Today.css';
|
||||
|
||||
/* A year is loaded up front so stepping back a day costs no request. */
|
||||
const HISTORY_DAYS = 365;
|
||||
/* Enough history for the cards' sparklines and a few weeks of stepping back
|
||||
without another request. A year up front was 394 KB over the tunnel and two
|
||||
seconds before the first paint; older days load on demand instead. */
|
||||
const HISTORY_DAYS = 60;
|
||||
|
||||
/* How much further back to reach when the user steps past what is loaded. */
|
||||
const EXTEND_DAYS = 180;
|
||||
|
||||
interface RingSpec {
|
||||
label: string;
|
||||
@@ -130,25 +135,29 @@ function TodayPage() {
|
||||
const [days, setDays] = useState<HealthDay[]>([]);
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [extending, setExtending] = useState(false);
|
||||
const [from, setFrom] = useState(() => daysAgo(HISTORY_DAYS - 1));
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
// A year in one read: browsing back a day should not cost a request,
|
||||
// and the cards' sparklines need the surrounding days anyway.
|
||||
setDays(await apiClient.getHealthSummary(
|
||||
daysAgo(HISTORY_DAYS - 1), todayIso()
|
||||
));
|
||||
} catch (err: any) {
|
||||
setError(errorMessage(err, '加载数据失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
/* One loader for both the first paint and every widening of the window, so
|
||||
the two cannot disagree about what is loaded. */
|
||||
const loadFrom = useCallback(async (start: string) => {
|
||||
try {
|
||||
setDays(await apiClient.getHealthSummary(start, todayIso()));
|
||||
setFrom(start);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
setError(errorMessage(err, '加载数据失败'));
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadFrom(daysAgo(HISTORY_DAYS - 1));
|
||||
}, [loadFrom]);
|
||||
|
||||
const newest = days.length ? days[days.length - 1].date : null;
|
||||
const date = selected ?? newest;
|
||||
const index = date ? days.findIndex((d) => d.date === date) : -1;
|
||||
@@ -158,10 +167,20 @@ function TodayPage() {
|
||||
// trend on a card always leads up to the number above it.
|
||||
const history = index >= 0 ? days.slice(Math.max(0, index - 13), index + 1) : [];
|
||||
|
||||
const oldest = days.length ? days[0].date : null;
|
||||
const canPrev = !!(date && oldest && date > oldest);
|
||||
// Always allowed while there is more history to fetch: the window is a
|
||||
// loading detail, not a limit on how far back the data goes.
|
||||
const canPrev = !!date && !extending;
|
||||
const canNext = !!(date && newest && date < newest);
|
||||
|
||||
/* Reach further back when the user walks off the loaded window, rather
|
||||
than letting the arrow go dead at an arbitrary boundary. */
|
||||
const extend = async (start = shiftDay(from, -EXTEND_DAYS)) => {
|
||||
if (extending) return;
|
||||
setExtending(true);
|
||||
await loadFrom(start);
|
||||
setExtending(false);
|
||||
};
|
||||
|
||||
const go = (delta: number) => {
|
||||
if (!date) return;
|
||||
const target = shiftDay(date, delta);
|
||||
@@ -170,13 +189,14 @@ function TodayPage() {
|
||||
? [...days].reverse().find((d) => d.date <= target)
|
||||
: days.find((d) => d.date >= target);
|
||||
if (nearest) setSelected(nearest.date);
|
||||
else if (delta < 0) extend();
|
||||
};
|
||||
|
||||
const openCalendar = () => {
|
||||
if (!date) return;
|
||||
const calendar = f7.calendar.create({
|
||||
value: [new Date(`${date}T12:00:00`)],
|
||||
minDate: oldest ? new Date(`${oldest}T12:00:00`) : undefined,
|
||||
minDate: undefined,
|
||||
maxDate: newest ? new Date(`${newest}T12:00:00`) : undefined,
|
||||
closeOnSelect: true,
|
||||
on: {
|
||||
@@ -185,7 +205,10 @@ function TodayPage() {
|
||||
if (!values?.length) return;
|
||||
const picked = iso(values[0]);
|
||||
const match = days.find((d) => d.date === picked);
|
||||
setSelected(match ? match.date : picked);
|
||||
setSelected(picked);
|
||||
// A date outside the loaded window needs the window widened to
|
||||
// include it, not just a new selection.
|
||||
if (!match) extend(picked);
|
||||
},
|
||||
closed(c: any) { c.destroy(); },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user