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 = () => (
+
-
-
-
+ {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],
);