Skip to content

Commit 3eeed9c

Browse files
Merge pull request #224 from rocky-linux/223-bug-download-page---architecture-tabs-response-is-very-late
Fix #223: Optimize download page architecture tab switching performance
2 parents c117354 + baa8478 commit 3eeed9c

File tree

4 files changed

+364
-235
lines changed

4 files changed

+364
-235
lines changed

.mcp.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mcpServers": {
3+
"playwright": {
4+
"type": "stdio",
5+
"command": "npx",
6+
"args": ["@playwright/mcp@latest", "--extension"],
7+
"env": {}
8+
}
9+
}
10+
}

app/[locale]/download/components/Tabs.tsx

Lines changed: 7 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useTranslations } from "next-intl";
22

33
import TabsClient from "./TabsClient";
4+
import { processArchitecturesData } from "@/utils/downloadDataProcessor";
45

56
import type { DownloadData } from "@/types/downloads";
67

@@ -71,6 +72,8 @@ const DownloadTabs = ({ downloadData }: DownloadTabsProps) => {
7172
},
7273
wslImages: {
7374
title: t("cards.wslImages.title"),
75+
download: t("cards.wslImages.download"),
76+
readMe: t("cards.wslImages.readMe"),
7477
},
7578
visionfive2Images: {
7679
title: t("cards.visionfive2Images.title"),
@@ -80,204 +83,10 @@ const DownloadTabs = ({ downloadData }: DownloadTabsProps) => {
8083
},
8184
};
8285

83-
// Transform raw data into client-ready format
84-
const processedArchitectures = Object.fromEntries(
85-
Object.entries(downloadData.architectures).map(([arch, data]) => [
86-
arch,
87-
{
88-
versions: data.versions.map((version) => {
89-
// Create a combined version with all the different mappings
90-
const baseVersion = {
91-
versionName: version.versionName,
92-
versionId: version.versionId,
93-
currentVersion: version.currentVersion,
94-
plannedEol: version.plannedEol,
95-
};
96-
97-
const defaultImages = {
98-
downloadOptions: [
99-
{
100-
label: translations.cards.defaultImages.downloadOptions.dvd,
101-
link: version.downloadOptions.defaultImages.dvd,
102-
},
103-
{
104-
label: translations.cards.defaultImages.downloadOptions.boot,
105-
link: version.downloadOptions.defaultImages.boot,
106-
},
107-
...(version.downloadOptions.defaultImages.minimal
108-
? [
109-
{
110-
label:
111-
translations.cards.defaultImages.downloadOptions
112-
.minimal,
113-
link: version.downloadOptions.defaultImages
114-
.minimal as string,
115-
},
116-
]
117-
: []),
118-
],
119-
links: [
120-
{
121-
name: translations.cards.defaultImages.torrent,
122-
link: version.links.defaultImages.torrent,
123-
},
124-
{
125-
name: translations.cards.defaultImages.checksum,
126-
link: version.links.defaultImages.checksum,
127-
},
128-
{
129-
name: translations.cards.defaultImages.baseOs,
130-
link: version.links.defaultImages.baseOs,
131-
},
132-
{
133-
name: translations.cards.defaultImages.archived,
134-
link: version.links.defaultImages.archived,
135-
},
136-
],
137-
};
138-
139-
const cloudImages = {
140-
downloadOptions:
141-
version.downloadOptions.cloudImages && version.links.cloudImages
142-
? [
143-
{
144-
label:
145-
translations.cards.cloudImages.downloadOptions.qcow2,
146-
link: version.downloadOptions.cloudImages.qcow2,
147-
},
148-
]
149-
: [],
150-
links:
151-
version.downloadOptions.cloudImages && version.links.cloudImages
152-
? [
153-
{
154-
name: translations.cards.defaultImages.checksum,
155-
link: version.links.cloudImages.checksum,
156-
},
157-
]
158-
: [],
159-
};
160-
161-
const containerImages = {
162-
downloadOptions: [
163-
{
164-
label: translations.cards.container.downloadOptions.fullImage,
165-
link: version.downloadOptions.container.fullImage,
166-
},
167-
{
168-
label:
169-
translations.cards.container.downloadOptions.minimalImage,
170-
link: version.downloadOptions.container.minimalImage,
171-
},
172-
],
173-
links: [],
174-
};
175-
176-
const liveImages = {
177-
downloadOptions: version.downloadOptions.liveImages
178-
? Object.entries(version.downloadOptions.liveImages).map(
179-
([key, link]) => ({
180-
label: t(`cards.liveImages.downloadOptions.${key}`),
181-
link: link as string,
182-
})
183-
)
184-
: [],
185-
links: version.links.liveImages
186-
? [
187-
{
188-
name: translations.cards.defaultImages.checksums,
189-
link: version.links.liveImages.checksums,
190-
},
191-
]
192-
: [],
193-
};
194-
195-
const rpiImages = {
196-
downloadOptions: version.downloadOptions.rpiImages
197-
? [
198-
{
199-
label: translations.cards.rpiImages.download,
200-
link: version.downloadOptions.rpiImages.download,
201-
},
202-
]
203-
: [],
204-
links: version.links.rpiImages
205-
? [
206-
{
207-
name: translations.cards.defaultImages.checksum,
208-
link: version.links.rpiImages.checksum,
209-
},
210-
{
211-
name: translations.cards.rpiImages.readMe,
212-
link: version.links.rpiImages.readMe,
213-
},
214-
]
215-
: [],
216-
};
217-
218-
const wslImages = {
219-
downloadOptions: version.downloadOptions.wslImages
220-
? [
221-
{
222-
label: translations.cards.rpiImages.download,
223-
link: version.downloadOptions.wslImages.download,
224-
},
225-
]
226-
: [],
227-
links: version.links.wslImages
228-
? [
229-
{
230-
name: translations.cards.defaultImages.checksum,
231-
link: version.links.wslImages.checksum,
232-
},
233-
{
234-
name: translations.cards.rpiImages.readMe,
235-
link: version.links.wslImages.readMe,
236-
},
237-
]
238-
: [],
239-
};
240-
241-
const visionFive2Images = {
242-
downloadOptions: version.downloadOptions.visionfive2Images
243-
? [
244-
{
245-
label: translations.cards.visionfive2Images.download,
246-
link: version.downloadOptions.visionfive2Images.download,
247-
},
248-
]
249-
: [],
250-
links: version.links.visionfive2Images
251-
? [
252-
{
253-
name: translations.cards.defaultImages.checksum,
254-
link: version.links.visionfive2Images.checksum,
255-
},
256-
...(version.links.visionfive2Images.readMe
257-
? [
258-
{
259-
name: translations.cards.visionfive2Images.readMe,
260-
link: version.links.visionfive2Images.readMe,
261-
},
262-
]
263-
: []),
264-
]
265-
: [],
266-
};
267-
268-
return {
269-
...baseVersion,
270-
defaultImages,
271-
cloudImages,
272-
containerImages,
273-
liveImages,
274-
rpiImages,
275-
wslImages,
276-
visionFive2Images,
277-
};
278-
}),
279-
},
280-
])
86+
// Transform raw data into client-ready format using cached utility
87+
const processedArchitectures = processArchitecturesData(
88+
downloadData,
89+
translations
28190
);
28291

28392
return (

app/[locale]/download/components/TabsClient.tsx

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

3-
import { useEffect, useState, useRef } from "react";
4-
import { useRouter, useSearchParams, usePathname } from "next/navigation";
3+
import { useEffect, useState, startTransition } from "react";
4+
import { useSearchParams, usePathname } from "next/navigation";
55
import { detectArchitecture } from "@/utils/architectureDetection";
66

77
import DefaultImageCard from "./DefaultImage/Card";
@@ -15,7 +15,6 @@ import {
1515
SelectTrigger,
1616
SelectValue,
1717
} from "@/components/ui/select";
18-
import { Route } from "next";
1918

2019
interface ProcessedVersion {
2120
versionName: string;
@@ -112,18 +111,22 @@ interface TabsClientProps {
112111
}
113112

114113
const TabsClient = ({ architectures, translations }: TabsClientProps) => {
115-
const router = useRouter();
116114
const searchParams = useSearchParams();
117115
const pathname = usePathname();
118116
const availableArchitectures = Object.keys(architectures);
119-
const [urlKey, setUrlKey] = useState(0);
120-
const isInitialLoad = useRef(true);
121117

122118
const archFromUrl = searchParams.get("arch");
123119

124120
// Use a stable default for initial render to avoid hydration mismatch
125121
const [detectedArch, setDetectedArch] = useState("x86_64");
126122

123+
// Client-side state for current architecture (for instant switching)
124+
const [clientArch, setClientArch] = useState<string | null>(() => {
125+
return archFromUrl && availableArchitectures.includes(archFromUrl)
126+
? archFromUrl
127+
: null;
128+
});
129+
127130
// Detect architecture only on client after hydration
128131
useEffect(() => {
129132
const detected = detectArchitecture();
@@ -136,49 +139,35 @@ const TabsClient = ({ architectures, translations }: TabsClientProps) => {
136139
? detectedArch
137140
: "x86_64";
138141

139-
const currentArch = availableArchitectures.includes(archFromUrl ?? "")
142+
const urlArch = availableArchitectures.includes(archFromUrl ?? "")
140143
? (archFromUrl ?? defaultArch)
141144
: defaultArch;
142145

146+
// Use client-side state for current architecture, fallback to URL or default
147+
const currentArch = clientArch ?? urlArch;
148+
143149
const updateArchitecture = (newArch: string) => {
144150
if (!availableArchitectures.includes(newArch)) return;
145151

152+
// Immediately update client state for instant UI response
153+
startTransition(() => {
154+
setClientArch(newArch);
155+
});
156+
157+
// Update URL without triggering server navigation (shallow update)
146158
const params = new URLSearchParams(searchParams.toString());
147159
params.set("arch", newArch);
160+
const newUrl = `${pathname}?${params.toString()}`;
148161

149-
// Use push to maintain history for user-initiated changes
150-
router.push(`${pathname}?${params.toString()}` as Route, { scroll: false });
162+
// Use window.history.pushState for shallow update without server round-trip
163+
window.history.pushState(null, "", newUrl);
151164
};
152165

153-
// Set default architecture on initial load (only if no arch in URL)
166+
// Sync client state with URL changes (including browser navigation)
154167
useEffect(() => {
155-
if (
156-
isInitialLoad.current &&
157-
!archFromUrl &&
158-
availableArchitectures.length > 0
159-
) {
160-
// Don't redirect - just set the internal state
161-
isInitialLoad.current = false;
162-
}
163-
}, [archFromUrl, availableArchitectures, isInitialLoad]);
164-
165-
// Handle browser back/forward navigation
166-
useEffect(() => {
167-
const handlePopState = () => {
168-
// Check if we're trying to go back to a different page
169-
const currentUrl = window.location.pathname + window.location.search;
170-
const isStillOnDownloadsPage = currentUrl.startsWith(pathname);
171-
172-
if (isStillOnDownloadsPage) {
173-
// Still on downloads page, just architecture changed
174-
setUrlKey((prevKey) => prevKey + 1);
175-
}
176-
// If not on downloads page, let the browser handle it naturally
177-
};
178-
179-
window.addEventListener("popstate", handlePopState);
180-
return () => window.removeEventListener("popstate", handlePopState);
181-
}, [pathname, urlKey]);
168+
// Clear client state when URL changes to let URL take precedence
169+
setClientArch(null);
170+
}, [searchParams]);
182171

183172
return (
184173
<Tabs

0 commit comments

Comments
 (0)