From c9a0dd50fef11027e980c30e1befb9ddf7676d9c Mon Sep 17 00:00:00 2001 From: ronload <91997734+ronload@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:42:59 +0800 Subject: [PATCH 1/3] perf(map): defer map chunk loading to unblock main thread Mobile/tablet: skip rendering TaiwanMap entirely via JS media query instead of CSS hidden, so the 1MB maplibre-gl chunk is never downloaded. Desktop: show a placeholder first and defer mounting until the main thread is idle (requestIdleCallback with setTimeout fallback for Safari), letting the address form become interactive immediately. --- src/components/address-page.tsx | 45 +++++++++++++++++++++++++++------ src/hooks/use-media-query.ts | 16 ++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/hooks/use-media-query.ts diff --git a/src/components/address-page.tsx b/src/components/address-page.tsx index 996f8bd..a5f5d3c 100644 --- a/src/components/address-page.tsx +++ b/src/components/address-page.tsx @@ -1,8 +1,10 @@ "use client"; +import { useEffect, useState } from "react"; import dynamic from "next/dynamic"; import type { City } from "@/lib/types"; import { useAddressState } from "@/hooks/use-address-state"; +import { useMediaQuery } from "@/hooks/use-media-query"; import { AddressForm } from "@/components/address-form"; const TaiwanMap = dynamic( @@ -15,25 +17,52 @@ const TaiwanMap = dynamic( }, ); +const MapPlaceholder = () => ( +
+); + interface AddressPageProps { cities: City[]; } export function AddressPage({ cities }: AddressPageProps) { const addressState = useAddressState(); + const isDesktop = useMediaQuery("(min-width: 1024px)"); + const [mapReady, setMapReady] = useState(false); + + useEffect(() => { + if (!isDesktop) { + setMapReady(false); + return; + } + + if (typeof requestIdleCallback === "function") { + const id = requestIdleCallback(() => setMapReady(true)); + return () => cancelIdleCallback(id); + } + + const id = setTimeout(() => setMapReady(true), 0); + return () => clearTimeout(id); + }, [isDesktop]); return (
-
-
- + {isDesktop && ( +
+
+ {mapReady ? ( + + ) : ( + + )} +
-
+ )}
); } diff --git a/src/hooks/use-media-query.ts b/src/hooks/use-media-query.ts new file mode 100644 index 0000000..ac62060 --- /dev/null +++ b/src/hooks/use-media-query.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const mql = window.matchMedia(query); + setMatches(mql.matches); + + const handler = (e: MediaQueryListEvent) => setMatches(e.matches); + mql.addEventListener("change", handler); + return () => mql.removeEventListener("change", handler); + }, [query]); + + return matches; +} From 18ee306bd0db20296dbe90e21ab5487a56457b10 Mon Sep 17 00:00:00 2001 From: ronload <91997734+ronload@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:44:50 +0800 Subject: [PATCH 2/3] fix(map): resolve react-hooks/set-state-in-effect lint errors Replace useMediaQuery implementation with useSyncExternalStore to avoid synchronous setState in effect. Remove unnecessary setMapReady reset since the isDesktop guard already prevents map rendering. --- src/components/address-page.tsx | 13 +++++-------- src/hooks/use-media-query.ts | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/components/address-page.tsx b/src/components/address-page.tsx index a5f5d3c..6073535 100644 --- a/src/components/address-page.tsx +++ b/src/components/address-page.tsx @@ -31,18 +31,15 @@ export function AddressPage({ cities }: AddressPageProps) { const [mapReady, setMapReady] = useState(false); useEffect(() => { - if (!isDesktop) { - setMapReady(false); - return; - } + if (!isDesktop) return; if (typeof requestIdleCallback === "function") { - const id = requestIdleCallback(() => setMapReady(true)); - return () => cancelIdleCallback(id); + const id = requestIdleCallback(() => { setMapReady(true); }); + return () => { cancelIdleCallback(id); }; } - const id = setTimeout(() => setMapReady(true), 0); - return () => clearTimeout(id); + const id = setTimeout(() => { setMapReady(true); }, 0); + return () => { clearTimeout(id); }; }, [isDesktop]); return ( diff --git a/src/hooks/use-media-query.ts b/src/hooks/use-media-query.ts index ac62060..80c6ff4 100644 --- a/src/hooks/use-media-query.ts +++ b/src/hooks/use-media-query.ts @@ -1,16 +1,16 @@ -import { useEffect, useState } from "react"; +import { useCallback, useSyncExternalStore } from "react"; export function useMediaQuery(query: string): boolean { - const [matches, setMatches] = useState(false); + const subscribe = useCallback( + (callback: () => void) => { + const mql = window.matchMedia(query); + mql.addEventListener("change", callback); + return () => { mql.removeEventListener("change", callback); }; + }, + [query], + ); - useEffect(() => { - const mql = window.matchMedia(query); - setMatches(mql.matches); + const getSnapshot = () => window.matchMedia(query).matches; - const handler = (e: MediaQueryListEvent) => setMatches(e.matches); - mql.addEventListener("change", handler); - return () => mql.removeEventListener("change", handler); - }, [query]); - - return matches; + return useSyncExternalStore(subscribe, getSnapshot, () => false); } From 78b337f83a03a24d23c92baa41639bb9ae5d94c7 Mon Sep 17 00:00:00 2001 From: ronload <91997734+ronload@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:45:14 +0800 Subject: [PATCH 3/3] style: format code style --- src/components/address-page.tsx | 16 ++++++++++++---- src/hooks/use-media-query.ts | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/address-page.tsx b/src/components/address-page.tsx index 6073535..efad36e 100644 --- a/src/components/address-page.tsx +++ b/src/components/address-page.tsx @@ -34,12 +34,20 @@ export function AddressPage({ cities }: AddressPageProps) { if (!isDesktop) return; if (typeof requestIdleCallback === "function") { - const id = requestIdleCallback(() => { setMapReady(true); }); - return () => { cancelIdleCallback(id); }; + const id = requestIdleCallback(() => { + setMapReady(true); + }); + return () => { + cancelIdleCallback(id); + }; } - const id = setTimeout(() => { setMapReady(true); }, 0); - return () => { clearTimeout(id); }; + const id = setTimeout(() => { + setMapReady(true); + }, 0); + return () => { + clearTimeout(id); + }; }, [isDesktop]); return ( diff --git a/src/hooks/use-media-query.ts b/src/hooks/use-media-query.ts index 80c6ff4..09bdcbe 100644 --- a/src/hooks/use-media-query.ts +++ b/src/hooks/use-media-query.ts @@ -5,7 +5,9 @@ export function useMediaQuery(query: string): boolean { (callback: () => void) => { const mql = window.matchMedia(query); mql.addEventListener("change", callback); - return () => { mql.removeEventListener("change", callback); }; + return () => { + mql.removeEventListener("change", callback); + }; }, [query], );