perf: optimize bundle and load times via dynamic CSS imports, Webpack vendor chunking, and conditional map loading.(peak male content)
Build and Release / release (push) Successful in 37s

This commit is contained in:
taDuc
2026-06-04 10:21:23 +07:00
parent fb816fca11
commit 6c509a6b54
6 changed files with 147 additions and 126 deletions
+33 -1
View File
@@ -27,11 +27,43 @@ const nextConfig: NextConfig = {
], ],
}, },
output: 'standalone', output: 'standalone',
webpack(config) { webpack(config, { isServer }) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,
use: ["@svgr/webpack"], use: ["@svgr/webpack"],
}); });
if (!isServer) {
// Split heavy third-party vendor libraries into their own dedicated chunks
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
maplibre: {
test: /[\\/]node_modules[\\/]maplibre-gl[\\/]/,
name: "maplibre",
chunks: "all",
priority: 40,
},
quill: {
test: /[\\/]node_modules[\\/](react-quill-new|quill|quill-blot-formatter)[\\/]/,
name: "quill",
chunks: "all",
priority: 35,
},
charts: {
test: /[\\/]node_modules[\\/](apexcharts|react-apexcharts)[\\/]/,
name: "charts",
chunks: "all",
priority: 30,
},
calendar: {
test: /[\\/]node_modules[\\/]@fullcalendar[\\/]/,
name: "calendar",
chunks: "all",
priority: 25,
},
};
}
return config; return config;
}, },
+1 -89
View File
@@ -3,98 +3,10 @@
import React from "react"; import React from "react";
interface MapPlaceholderProps { interface MapPlaceholderProps {
isLoaderOnly?: boolean;
onEnter?: () => void; onEnter?: () => void;
} }
export default function MapPlaceholder({ isLoaderOnly = true, onEnter }: MapPlaceholderProps) { export default function MapPlaceholder({ onEnter }: MapPlaceholderProps) {
if (isLoaderOnly) {
return (
<div
style={{
position: "fixed",
inset: 0,
backgroundColor: "#060a13",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
zIndex: 9999,
}}
>
{/* Background Image */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/map_placeholder.webp"
alt="Map Loading Placeholder"
fetchPriority="high"
loading="eager"
decoding="sync"
width={1920}
height={1080}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
zIndex: 1,
filter: "brightness(0.3) contrast(1.1)",
backgroundImage: "url('data:image/webp;base64,UklGRmgAAABXRUJQVlA4IFwAAAAQAgCdASoQAAkAAgA0JbACdAD0j9ruBiEAAP71e6Hb3PzxBzEI1XGXkdQ3Wq1ek8XLa1nPPm65FhrFIjmR0%2BxZwNUJBvg15I7CuzvhuunZ%2FUF83IaP8Evo6gAAAA%3D%3D')",
backgroundSize: "cover",
}}
/>
{/* Dark overlay & Spinner */}
<div
style={{
position: "relative",
zIndex: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "16px",
backgroundColor: "rgba(6, 10, 19, 0.8)",
padding: "24px 32px",
borderRadius: "16px",
backdropFilter: "blur(8px)",
border: "1px solid rgba(217, 119, 6, 0.15)",
boxShadow: "0 20px 40px rgba(0,0,0,0.6)",
}}
>
<div
style={{
width: "36px",
height: "36px",
borderRadius: "50%",
border: "3px solid rgba(217, 119, 6, 0.1)",
borderTopColor: "#d97706",
animation: "placeholder-spin 1.2s linear infinite",
}}
/>
<style dangerouslySetInnerHTML={{ __html: `
@keyframes placeholder-spin {
to { transform: rotate(360deg); }
}
`}} />
<span
style={{
fontSize: "11px",
fontWeight: 600,
letterSpacing: "0.15em",
color: "#eab308",
textShadow: "0 0 10px rgba(234, 179, 7, 0.3)",
fontFamily: "Georgia, serif",
}}
>
ĐANG TẢI DỮ LIỆU...
</span>
</div>
</div>
);
}
return ( return (
<div <div
onClick={onEnter} onClick={onEnter}
@@ -49,6 +49,8 @@ type Props = {
overlay?: ReactNode; overlay?: ReactNode;
children?: ReactNode; children?: ReactNode;
onLoad?: () => void; onLoad?: () => void;
instantLoad?: boolean;
onToggleInstantLoad?: (val: boolean) => void;
}; };
export default function PreviewMapShell({ export default function PreviewMapShell({
@@ -88,6 +90,8 @@ export default function PreviewMapShell({
overlay, overlay,
children, children,
onLoad, onLoad,
instantLoad = true,
onToggleInstantLoad,
}: Props) { }: Props) {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -238,6 +242,22 @@ export default function PreviewMapShell({
</svg> </svg>
</button> </button>
<button
type="button"
onClick={() => onToggleInstantLoad?.(!instantLoad)}
title="Bật lên để load nhanh hơn"
style={{
...menuOptionStyle,
color: instantLoad ? "#fbbf24" : "#cbd5e1",
border: instantLoad ? "1px solid rgba(251, 191, 36, 0.4)" : "1px solid rgba(255, 255, 255, 0.08)",
boxShadow: instantLoad ? "0 0 12px rgba(251, 191, 36, 0.25)" : "0 2px 8px rgba(0, 0, 0, 0.12)",
}}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill={instantLoad ? "#fbbf24" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</button>
<button <button
type="button" type="button"
onClick={() => { window.location.href = "/faq"; }} onClick={() => { window.location.href = "/faq"; }}
@@ -36,9 +36,16 @@ const CURRENT_YEAR = new Date().getUTCFullYear();
interface PublicPreviewClientPageProps { interface PublicPreviewClientPageProps {
userHasEntered: boolean; userHasEntered: boolean;
onEnter: () => void; onEnter: () => void;
instantLoad: boolean;
toggleInstantLoad: (val: boolean) => void;
} }
export default function PublicPreviewClientPage({ userHasEntered, onEnter }: PublicPreviewClientPageProps) { export default function PublicPreviewClientPage({
userHasEntered,
onEnter,
instantLoad,
toggleInstantLoad
}: PublicPreviewClientPageProps) {
const [selectedFeatureIds, setSelectedFeatureIds] = useState<(string | number)[]>([]); const [selectedFeatureIds, setSelectedFeatureIds] = useState<(string | number)[]>([]);
const [timelineYear, setTimelineYear] = useState<number>(1000); const [timelineYear, setTimelineYear] = useState<number>(1000);
const [timelineDraftYear, setTimelineDraftYear] = useState<number>(1000); const [timelineDraftYear, setTimelineDraftYear] = useState<number>(1000);
@@ -64,7 +71,6 @@ export default function PublicPreviewClientPage({ userHasEntered, onEnter }: Pub
}); });
const [isLargeScreen, setIsLargeScreen] = useState(false); const [isLargeScreen, setIsLargeScreen] = useState(false);
const [loadInteractiveMap, setLoadInteractiveMap] = useState(false); const [loadInteractiveMap, setLoadInteractiveMap] = useState(false);
const [isMapLoaded, setIsMapLoaded] = useState(false);
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -78,8 +84,22 @@ export default function PublicPreviewClientPage({ userHasEntered, onEnter }: Pub
} }
} }
} }
setLoadInteractiveMap(true);
}, []); if (instantLoad) {
setLoadInteractiveMap(true);
} else {
const timer = setTimeout(() => {
setLoadInteractiveMap(true);
}, 2000);
return () => clearTimeout(timer);
}
}, [instantLoad]);
useEffect(() => {
if (userHasEntered) {
setLoadInteractiveMap(true);
}
}, [userHasEntered]);
const mapHandleRef = useRef<MapHandle>(null); const mapHandleRef = useRef<MapHandle>(null);
const isFirstMount = useRef(true); const isFirstMount = useRef(true);
@@ -374,6 +394,8 @@ export default function PublicPreviewClientPage({ userHasEntered, onEnter }: Pub
labelTimelineYear={currentTimelineYear} labelTimelineYear={currentTimelineYear}
selectedFeatureIds={selectedFeatureIds} selectedFeatureIds={selectedFeatureIds}
onSelectFeatureIds={setSelectedFeatureIds} onSelectFeatureIds={setSelectedFeatureIds}
instantLoad={instantLoad}
onToggleInstantLoad={toggleInstantLoad}
backgroundVisibility={backgroundVisibility} backgroundVisibility={backgroundVisibility}
geometryVisibility={geometryVisibility} geometryVisibility={geometryVisibility}
onToggleBackground={handleToggleBackgroundLayer} onToggleBackground={handleToggleBackgroundLayer}
@@ -403,7 +425,6 @@ export default function PublicPreviewClientPage({ userHasEntered, onEnter }: Pub
maxSidebarDragWidth={maxDragWidth} maxSidebarDragWidth={maxDragWidth}
onPlayPreviewReplay={activeReplay && replayMode === "idle" ? handlePlayPreviewReplay : undefined} onPlayPreviewReplay={activeReplay && replayMode === "idle" ? handlePlayPreviewReplay : undefined}
timelineDisabled={replayMode === "playing"} timelineDisabled={replayMode === "playing"}
onLoad={() => setIsMapLoaded(true)}
overlay={ overlay={
replayMode === "playing" ? ( replayMode === "playing" ? (
<ReplayPreviewOverlay <ReplayPreviewOverlay
@@ -459,13 +480,13 @@ export default function PublicPreviewClientPage({ userHasEntered, onEnter }: Pub
position: "fixed", position: "fixed",
inset: 0, inset: 0,
zIndex: 9999, zIndex: 9999,
pointerEvents: userHasEntered && isMapLoaded && isBackgroundVisibilityReady && loadInteractiveMap ? "none" : "auto", pointerEvents: userHasEntered ? "none" : "auto",
opacity: userHasEntered && isMapLoaded && isBackgroundVisibilityReady && loadInteractiveMap ? 0 : 1, opacity: userHasEntered ? 0 : 1,
visibility: userHasEntered && isMapLoaded && isBackgroundVisibilityReady && loadInteractiveMap ? "hidden" : "visible", visibility: userHasEntered ? "hidden" : "visible",
transition: "opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.8s", transition: "opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.8s",
}} }}
> >
<MapPlaceholder isLoaderOnly={userHasEntered} onEnter={onEnter} /> <MapPlaceholder onEnter={onEnter} />
</div> </div>
{linkEntityPopup ? ( {linkEntityPopup ? (
@@ -11,14 +11,31 @@ import MapPlaceholder from "./MapPlaceholder";
const LoaderComponent = () => { const LoaderComponent = () => {
const mapEntered = useSelector((state: RootState) => state.ui.mapEntered); const mapEntered = useSelector((state: RootState) => state.ui.mapEntered);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [instantLoad, setInstantLoad] = useState<boolean>(true);
useEffect(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("instant-load");
if (saved !== null) {
setInstantLoad(saved === "true");
}
}
}, []);
const handleEnter = () => { const handleEnter = () => {
dispatch(setMapEntered(true)); dispatch(setMapEntered(true));
}; };
const toggleInstantLoad = (val: boolean) => {
setInstantLoad(val);
if (typeof window !== "undefined") {
localStorage.setItem("instant-load", String(val));
}
window.dispatchEvent(new Event("instant-load-changed"));
};
return ( return (
<MapPlaceholder <MapPlaceholder
isLoaderOnly={mapEntered}
onEnter={handleEnter} onEnter={handleEnter}
/> />
); );
@@ -35,12 +52,36 @@ const PublicPreviewClientPage = dynamic(
export default function PublicPreviewWrapper() { export default function PublicPreviewWrapper() {
const mapEntered = useSelector((state: RootState) => state.ui.mapEntered); const mapEntered = useSelector((state: RootState) => state.ui.mapEntered);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [instantLoad, setInstantLoad] = useState<boolean>(true);
const [loadInteractive, setLoadInteractive] = useState(() => mapEntered); useEffect(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("instant-load");
if (saved !== null) {
setInstantLoad(saved === "true");
}
const handleSync = () => {
const updated = localStorage.getItem("instant-load");
if (updated !== null) {
setInstantLoad(updated === "true");
}
};
window.addEventListener("instant-load-changed", handleSync);
return () => window.removeEventListener("instant-load-changed", handleSync);
}
}, []);
const toggleInstantLoad = (val: boolean) => {
setInstantLoad(val);
if (typeof window !== "undefined") {
localStorage.setItem("instant-load", String(val));
}
window.dispatchEvent(new Event("instant-load-changed"));
};
const handleEnter = () => { const handleEnter = () => {
dispatch(setMapEntered(true)); dispatch(setMapEntered(true));
setLoadInteractive(true);
}; };
useEffect(() => { useEffect(() => {
@@ -54,36 +95,27 @@ export default function PublicPreviewWrapper() {
window.addEventListener("touchstart", handleInteraction, { passive: true, once: true }); window.addEventListener("touchstart", handleInteraction, { passive: true, once: true });
window.addEventListener("keydown", handleInteraction, { passive: true, once: true }); window.addEventListener("keydown", handleInteraction, { passive: true, once: true });
// Tải ngầm bản đồ sau 2.5 giây khi trình duyệt rảnh rỗi (idle)
let idleId: any = null;
const prefetchTimer = setTimeout(() => {
const runPrefetch = () => {
setLoadInteractive(true);
};
if (typeof window !== "undefined") {
if ("requestIdleCallback" in window) {
idleId = (window as any).requestIdleCallback(runPrefetch, { timeout: 3000 });
} else {
setTimeout(runPrefetch, 200);
}
}
}, 2000);
return () => { return () => {
window.removeEventListener("click", handleInteraction); window.removeEventListener("click", handleInteraction);
window.removeEventListener("touchstart", handleInteraction); window.removeEventListener("touchstart", handleInteraction);
window.removeEventListener("keydown", handleInteraction); window.removeEventListener("keydown", handleInteraction);
clearTimeout(prefetchTimer);
if (idleId && typeof window !== "undefined" && "cancelIdleCallback" in window) {
(window as any).cancelIdleCallback(idleId);
}
}; };
}, [mapEntered]); }, [mapEntered]);
if (!loadInteractive) { if (!mapEntered && !instantLoad) {
return <MapPlaceholder isLoaderOnly={false} onEnter={handleEnter} />; return (
<MapPlaceholder
onEnter={handleEnter}
/>
);
} }
return <PublicPreviewClientPage userHasEntered={mapEntered} onEnter={handleEnter} />; return (
<PublicPreviewClientPage
userHasEntered={mapEntered}
onEnter={handleEnter}
instantLoad={instantLoad}
toggleInstantLoad={toggleInstantLoad}
/>
);
} }
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useRef, useState, memo } from "react"; import { useEffect, useMemo, useRef, useState, memo } from "react";
import "react-quill-new/dist/quill.snow.css"; // Loaded dynamically inside the component to prevent render-blocking
import type { Entity } from "@/uhm/api/entities"; import type { Entity } from "@/uhm/api/entities";
import type { Wiki } from "@/uhm/api/wikis"; import type { Wiki } from "@/uhm/api/wikis";
@@ -139,6 +139,10 @@ function PublicWikiSidebar({
const contentRootRef = useRef<HTMLDivElement | null>(null); const contentRootRef = useRef<HTMLDivElement | null>(null);
const tocContainerRef = useRef<HTMLDivElement | null>(null); const tocContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
import("react-quill-new/dist/quill.snow.css");
}, []);
const [localWidth, setLocalWidth] = useState<number>(() => { const [localWidth, setLocalWidth] = useState<number>(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const saved = localStorage.getItem("public-wiki-sidebar-width"); const saved = localStorage.getItem("public-wiki-sidebar-width");