22 KiB
First Screen Lazy Heavy Dependencies Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Keep Mermaid, Leaflet, React Leaflet, marker cluster, and feature CSS out of the auth/signup initial screen while preserving diagram, math, and map behavior when those features are used.
Architecture: Move runtime Leaflet objects behind plain coordinate data at parent boundaries, then lazy-load map implementations from small wrapper components. Replace Mermaid's static import with effect-time dynamic import, and move KaTeX/Leaflet CSS from the app entry into feature modules.
Tech Stack: React 19, TypeScript 6, Vite 8/Rolldown, React Router 7, React Query 5, Mermaid, KaTeX, Leaflet, React Leaflet, pnpm.
File Structure
- Create
web/src/components/map/types.ts- Defines a lightweight
MapPointinterface used by parents that must not import Leaflet.
- Defines a lightweight
- Create
web/src/components/map/LazyLocationPicker.tsx- Provides a
React.lazyboundary and map-sized fallback forLocationPicker.
- Provides a
- Modify
web/src/main.tsx- Removes global Leaflet and KaTeX CSS imports.
- Modify
web/src/router/index.tsx- Lazy-loads the Home route so memo/editor/markdown modules are not part of the auth/signup entry graph.
- Modify
web/vite.config.mts- Keeps optional heavy dependency split groups from becoming entry preloads.
- Modify
web/src/components/map/LocationPicker.tsx- Imports Leaflet CSS inside the lazy implementation path.
- Accepts and emits plain
MapPointvalues at the public component boundary. - Keeps
LatLngruntime use internal to the map implementation.
- Modify
web/src/components/map/index.ts- Stops exporting
LocationPickerand map utility helpers from the barrel so non-map imports do not pull Leaflet into parent chunks. - Keeps exporting
useReverseGeocodingbecause it has no Leaflet dependency.
- Stops exporting
- Modify
web/src/components/MemoEditor/types/insert-menu.ts- Replaces
LatLngin editor state withMapPoint.
- Replaces
- Modify
web/src/components/MemoEditor/hooks/useLocation.ts- Removes runtime Leaflet import and stores plain coordinates.
- Modify
web/src/components/MemoEditor/Toolbar/InsertMenu.tsx- Removes runtime Leaflet import.
- Imports
useReverseGeocodingdirectly from its file. - Constructs plain coordinates from geolocation.
- Modify
web/src/components/MemoMetadata/Location/LocationDialog.tsx- Uses
LazyLocationPickerandMapPoint. - Only mounts the lazy picker while the dialog is open.
- Uses
- Modify
web/src/components/MemoMetadata/Location/LocationDisplayView.tsx- Removes runtime Leaflet import and uses
LazyLocationPickerinside the popover.
- Removes runtime Leaflet import and uses
- Modify
web/src/pages/UserProfile.tsx- Lazy-loads
UserMemoMaponly when the map tab is active.
- Lazy-loads
- Modify
web/src/components/MemoContent/MemoMarkdownRenderer.tsx- Imports KaTeX CSS from the markdown-rendering path.
- Modify
web/src/components/MemoContent/MermaidBlock.tsx- Dynamically imports Mermaid only when rendering a Mermaid block.
- Modify
web/src/components/UserMemoMap/UserMemoMap.tsx- Keeps marker cluster CSS in the lazy map implementation path.
- Verify with
pnpm lint,pnpm build, and a production preview/browser network check.
Task 1: Remove Leaflet From Editor Location State
Files:
-
Create:
web/src/components/map/types.ts -
Modify:
web/src/components/MemoEditor/types/insert-menu.ts -
Modify:
web/src/components/MemoEditor/hooks/useLocation.ts -
Modify:
web/src/components/MemoEditor/Toolbar/InsertMenu.tsx -
Step 1: Add a Leaflet-free map coordinate type
Create web/src/components/map/types.ts:
export interface MapPoint {
lat: number;
lng: number;
}
- Step 2: Replace editor location state type
In web/src/components/MemoEditor/types/insert-menu.ts, replace the entire file with:
import type { MapPoint } from "@/components/map/types";
export interface LocationState {
placeholder: string;
position?: MapPoint;
latInput: string;
lngInput: string;
}
- Step 3: Remove Leaflet runtime usage from
useLocation
In web/src/components/MemoEditor/hooks/useLocation.ts, remove import { LatLng } from "leaflet";, add a type import, and use plain coordinate objects:
import { create } from "@bufbuild/protobuf";
import { useCallback, useMemo, useRef, useState } from "react";
import type { MapPoint } from "@/components/map/types";
import { Location, LocationSchema } from "@/types/proto/api/v1/memo_service_pb";
import { LocationState } from "../types/insert-menu";
export const useLocation = (initialLocation?: Location) => {
const [locationInitialized, setLocationInitialized] = useState(false);
const locationInitializedRef = useRef(locationInitialized);
locationInitializedRef.current = locationInitialized;
const [state, setState] = useState<LocationState>({
placeholder: initialLocation?.placeholder || "",
position: initialLocation ? { lat: initialLocation.latitude, lng: initialLocation.longitude } : undefined,
latInput: initialLocation ? String(initialLocation.latitude) : "",
lngInput: initialLocation ? String(initialLocation.longitude) : "",
});
const stateRef = useRef(state);
stateRef.current = state;
const updatePosition = useCallback((position?: MapPoint) => {
setState((prev) => ({
...prev,
position,
latInput: position ? String(position.lat) : "",
lngInput: position ? String(position.lng) : "",
}));
}, []);
const handlePositionChange = useCallback(
(position: MapPoint) => {
if (!locationInitializedRef.current) setLocationInitialized(true);
updatePosition(position);
},
[updatePosition],
);
const updateCoordinate = useCallback((type: "lat" | "lng", value: string) => {
const num = parseFloat(value);
const isValid = type === "lat" ? !isNaN(num) && num >= -90 && num <= 90 : !isNaN(num) && num >= -180 && num <= 180;
setState((prev) => {
const next = { ...prev, [type === "lat" ? "latInput" : "lngInput"]: value };
if (isValid && prev.position) {
const newPos = type === "lat" ? { lat: num, lng: prev.position.lng } : { lat: prev.position.lat, lng: num };
return { ...next, position: newPos, latInput: String(newPos.lat), lngInput: String(newPos.lng) };
}
return next;
});
}, []);
const setPlaceholder = useCallback((placeholder: string) => {
setState((prev) => ({ ...prev, placeholder }));
}, []);
const reset = useCallback(() => {
setState({
placeholder: "",
position: undefined,
latInput: "",
lngInput: "",
});
setLocationInitialized(false);
}, []);
const getLocation = useCallback((): Location | undefined => {
const { position, placeholder } = stateRef.current;
if (!position || !placeholder.trim()) {
return undefined;
}
return create(LocationSchema, {
latitude: position.lat,
longitude: position.lng,
placeholder,
});
}, []);
return useMemo(
() => ({ state, locationInitialized, handlePositionChange, updateCoordinate, setPlaceholder, reset, getLocation }),
[state, locationInitialized, handlePositionChange, updateCoordinate, setPlaceholder, reset, getLocation],
);
};
- Step 4: Remove Leaflet import from
InsertMenu
In web/src/components/MemoEditor/Toolbar/InsertMenu.tsx:
Remove:
import { LatLng } from "leaflet";
Replace:
import { useReverseGeocoding } from "@/components/map";
with:
import { useReverseGeocoding } from "@/components/map/useReverseGeocoding";
Replace the geolocation success handler body:
handleLocationPositionChange(new LatLng(position.coords.latitude, position.coords.longitude));
with:
handleLocationPositionChange({ lat: position.coords.latitude, lng: position.coords.longitude });
- Step 5: Run focused type check
Run:
cd web && pnpm lint
Expected: TypeScript may still fail because map component props have not been converted yet. If the only failures are LatLng/MapPoint mismatches in map/location components, continue to Task 2. If unrelated failures appear, stop and inspect before editing further.
Task 2: Lazy-Load Location Picker And Keep Leaflet Inside Map Modules
Files:
-
Create:
web/src/components/map/LazyLocationPicker.tsx -
Modify:
web/src/components/map/LocationPicker.tsx -
Modify:
web/src/components/map/index.ts -
Modify:
web/src/components/MemoMetadata/Location/LocationDialog.tsx -
Modify:
web/src/components/MemoMetadata/Location/LocationDisplayView.tsx -
Step 1: Create lazy location picker wrapper
Create web/src/components/map/LazyLocationPicker.tsx:
import { lazy, Suspense } from "react";
import { cn } from "@/lib/utils";
import type { MapPoint } from "./types";
interface LazyLocationPickerProps {
readonly?: boolean;
latlng?: MapPoint;
onChange?: (position: MapPoint) => void;
className?: string;
}
const LocationPicker = lazy(() => import("./LocationPicker"));
export const LazyLocationPicker = ({ className, ...props }: LazyLocationPickerProps) => {
return (
<Suspense
fallback={
<div
className={cn(
"memo-location-map relative isolate h-72 w-full overflow-hidden rounded-xl border border-border bg-muted/30 shadow-sm",
className,
)}
/>
}
>
<LocationPicker className={className} {...props} />
</Suspense>
);
};
- Step 2: Convert
LocationPickerpublic props toMapPoint
In web/src/components/map/LocationPicker.tsx:
Add CSS and type imports near the top:
import "leaflet/dist/leaflet.css";
import type { MapPoint } from "./types";
Keep Leaflet runtime import:
import L, { LatLng } from "leaflet";
Add helper functions after imports:
const toLatLng = (point: MapPoint): LatLng => new LatLng(point.lat, point.lng);
const fromLatLng = (latlng: LatLng): MapPoint => ({ lat: latlng.lat, lng: latlng.lng });
Update public-facing props:
interface LocationMarkerProps {
position: LatLng | undefined;
onChange: (position: MapPoint) => void;
readonly?: boolean;
}
In LocationMarker, replace:
onChange(e.latlng);
with:
onChange(fromLatLng(e.latlng));
Update MapControlsProps:
interface MapControlsProps {
position: MapPoint | undefined;
}
Update LocationPickerProps:
interface LocationPickerProps {
readonly?: boolean;
latlng?: MapPoint;
onChange?: (position: MapPoint) => void;
className?: string;
}
Replace:
const DEFAULT_CENTER_LAT_LNG = new LatLng(48.8584, 2.2945);
with:
const DEFAULT_CENTER: MapPoint = { lat: 48.8584, lng: 2.2945 };
Inside LocationPicker, replace:
const position = latlng || DEFAULT_CENTER_LAT_LNG;
with:
const position = latlng || DEFAULT_CENTER;
const mapCenter = toLatLng(position);
const markerPosition = latlng ? toLatLng(latlng) : mapCenter;
Replace the MapContainer props and marker call:
<MapContainer
className="h-full w-full !bg-muted"
center={mapCenter}
zoom={13}
scrollWheelZoom={false}
zoomControl={false}
attributionControl={false}
>
<ThemedTileLayer />
<LocationMarker position={markerPosition} readonly={readOnly} onChange={onChange} />
<MapControls position={latlng} />
<MapCleanup />
</MapContainer>
- Step 3: Keep the map barrel Leaflet-free
Replace web/src/components/map/index.ts with:
export { useReverseGeocoding } from "./useReverseGeocoding";
Do not export LocationPicker, LazyLocationPicker, map-utils, or Leaflet helpers from this barrel. Import map UI directly from @/components/map/LazyLocationPicker or the implementation file.
- Step 4: Use lazy picker in
LocationDialog
In web/src/components/MemoMetadata/Location/LocationDialog.tsx:
Remove:
import type { LatLng } from "leaflet";
import { LocationPicker } from "@/components/map";
Add:
import { LazyLocationPicker } from "@/components/map/LazyLocationPicker";
import type { MapPoint } from "@/components/map/types";
Change the prop type:
onPositionChange: (position: MapPoint) => void;
Replace:
<LocationPicker className="h-full" latlng={position} onChange={onPositionChange} />
with:
{open && <LazyLocationPicker className="h-full" latlng={position} onChange={onPositionChange} />}
- Step 5: Use lazy picker in
LocationDisplayView
In web/src/components/MemoMetadata/Location/LocationDisplayView.tsx:
Remove:
import { LatLng } from "leaflet";
import { LocationPicker } from "@/components/map";
Add:
import { LazyLocationPicker } from "@/components/map/LazyLocationPicker";
Replace:
<LocationPicker latlng={new LatLng(location.latitude, location.longitude)} readonly={true} />
with:
{popoverOpen && <LazyLocationPicker latlng={{ lat: location.latitude, lng: location.longitude }} readonly={true} />}
- Step 6: Run lint
Run:
cd web && pnpm lint
Expected: PASS for the files changed so far, or only failures unrelated to this work. Fix any MapPoint/LatLng type errors before continuing.
Task 3: Lazy-Load Profile Map
Files:
-
Modify:
web/src/pages/UserProfile.tsx -
Modify:
web/src/components/UserMemoMap/UserMemoMap.tsx -
Step 1: Move
UserMemoMapbehindReact.lazy
In web/src/pages/UserProfile.tsx, replace the React import section:
import copy from "copy-to-clipboard";
import { ExternalLinkIcon, LayoutListIcon, type LucideIcon, MapIcon } from "lucide-react";
import { toast } from "react-hot-toast";
with:
import copy from "copy-to-clipboard";
import { lazy, Suspense } from "react";
import { ExternalLinkIcon, LayoutListIcon, type LucideIcon, MapIcon } from "lucide-react";
import { toast } from "react-hot-toast";
Remove:
import UserMemoMap from "@/components/UserMemoMap";
Add after the type TabView = "memos" | "map"; line:
const UserMemoMap = lazy(() => import("@/components/UserMemoMap"));
Replace:
<div className="">
<UserMemoMap creator={user.name} className="h-[60dvh] sm:h-[500px] rounded-xl" />
</div>
with:
<div className="">
<Suspense fallback={<div className="h-[60dvh] sm:h-[500px] rounded-xl border border-border bg-muted/30" />}>
<UserMemoMap creator={user.name} className="h-[60dvh] sm:h-[500px] rounded-xl" />
</Suspense>
</div>
- Step 2: Keep Leaflet CSS in the lazy profile map implementation
In web/src/components/UserMemoMap/UserMemoMap.tsx, add the Leaflet CSS import above marker cluster CSS:
import "leaflet/dist/leaflet.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
The file should continue to import Leaflet, React Leaflet, marker clustering, and map-utils directly because this component is now loaded only from a lazy boundary.
- Step 3: Run lint
Run:
cd web && pnpm lint
Expected: PASS, or only unrelated pre-existing failures.
Task 4: Move KaTeX CSS And Dynamically Import Mermaid
Files:
-
Modify:
web/src/main.tsx -
Modify:
web/src/components/MemoContent/MemoMarkdownRenderer.tsx -
Modify:
web/src/components/MemoContent/MermaidBlock.tsx -
Step 1: Remove feature CSS from app entry
In web/src/main.tsx, remove:
import "leaflet/dist/leaflet.css";
import "katex/dist/katex.min.css";
- Step 2: Load KaTeX CSS from markdown renderer
In web/src/components/MemoContent/MemoMarkdownRenderer.tsx, add this import with the existing dependency imports:
import "katex/dist/katex.min.css";
- Step 3: Remove static Mermaid import
In web/src/components/MemoContent/MermaidBlock.tsx, remove:
import mermaid from "mermaid";
- Step 4: Replace Mermaid initialization/render effects with dynamic import
In web/src/components/MemoContent/MermaidBlock.tsx, remove the existing Mermaid initialization effect:
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
theme: toMermaidTheme(currentTheme),
securityLevel: "strict",
fontFamily: "inherit",
suppressErrorRendering: true,
});
}, [currentTheme]);
Replace the existing render effect with:
useEffect(() => {
if (!codeContent) return;
let cancelled = false;
const renderDiagram = async () => {
try {
const { default: mermaid } = await import("mermaid");
if (cancelled) return;
mermaid.initialize({
startOnLoad: false,
theme: toMermaidTheme(currentTheme),
securityLevel: "strict",
fontFamily: "inherit",
suppressErrorRendering: true,
});
const id = `mermaid-${Math.random().toString(36).substring(7)}`;
const { svg: renderedSvg } = await mermaid.render(id, codeContent);
if (cancelled) return;
setSvg(renderedSvg);
setError("");
} catch (err) {
if (cancelled) return;
console.error("Failed to render mermaid diagram:", err);
setSvg("");
setError(formatErrorMessage(err));
}
};
renderDiagram();
return () => {
cancelled = true;
};
}, [codeContent, currentTheme]);
- Step 5: Run lint
Run:
cd web && pnpm lint
Expected: PASS, or only unrelated pre-existing failures.
Task 4.5: Remove Remaining Entry Preloads Found During Verification
Files:
-
Modify:
web/src/router/index.tsx -
Modify:
web/vite.config.mts -
Step 1: Lazy-load the Home route
web/src/router/index.tsx now uses lazyWithReload(() => import("@/pages/Home")) instead of a static Home import. This prevents Home, PagedMemoList, MemoEditor, MemoContent, KaTeX, and Mermaid code from entering the auth/signup app entry graph.
- Step 2: Tighten optional vendor split groups
web/vite.config.mts no longer defines a manual mermaid-vendor group, because Rolldown emitted the preload helper from that group and forced an entry preload. The Leaflet group now matches only the leaflet package, not react-leaflet, so React does not get bundled into a Leaflet-named entry preload.
- Step 3: Re-run lint and build
Run:
cd web && pnpm lint && pnpm build
Expected: PASS.
Task 5: Build And Verify Initial Network Behavior
Files:
-
No source edits expected unless verification finds a regression.
-
Step 1: Build production frontend
Run:
cd web && pnpm build
Expected: PASS. Build output should still contain separate Mermaid and Leaflet chunks, but they should not be required by the auth/signup initial route.
- Step 2: Inspect build output for heavy chunks
Run:
cd web && find dist/assets -maxdepth 1 -type f \( -name '*mermaid*' -o -name '*leaflet*' -o -name '*katex*' \) -print | sort
Expected: Mermaid and Leaflet assets may exist as lazy chunks. Their existence is fine; the goal is that auth/signup does not request them initially.
- Step 3: Start production preview
Run:
cd web && pnpm exec vite preview --host 127.0.0.1 --port 4173
Expected: Preview server starts on http://127.0.0.1:4173/. Keep this session running until browser verification is complete.
- Step 4: Verify
/auth/signupnetwork with browser tooling
Open:
http://127.0.0.1:4173/auth/signup
Expected initial document and asset requests do not include filenames containing:
mermaid
leaflet
If KaTeX CSS still appears on /auth/signup, inspect the chunk initiator and remove any remaining static import path that reaches MemoMarkdownRenderer from auth/signup.
- Step 5: Smoke test feature lazy loading
Use the running app or a local backend/dev setup to verify:
1. A memo containing a Mermaid code block renders the diagram and requests the Mermaid chunk only when the memo content appears.
2. A memo containing inline or block math displays KaTeX styling when memo content appears.
3. Opening the location picker loads Leaflet assets and the picker remains interactive.
4. Opening a memo location popover loads Leaflet assets and shows the pinned map.
5. Opening `/u/:username?view=map` loads the profile map and marker cluster behavior still works.
Expected: Features behave as before after their lazy chunks load.
Result in this session: live feature smoke was not completed because the production preview has no authenticated backend session or seeded memo data. Static verification confirmed the relevant feature chunks still exist and load paths are behind lazy imports.
- Step 6: Stop preview server
Stop the preview command from Step 3 with Ctrl-C.
Task 6: Commit Implementation
Files:
-
All source files modified by Tasks 1-4.
-
Step 1: Review final diff
Run:
git diff -- web/src/main.tsx web/src/components/map web/src/components/MemoEditor web/src/components/MemoMetadata/Location web/src/pages/UserProfile.tsx web/src/components/MemoContent web/src/components/UserMemoMap/UserMemoMap.tsx
Expected: Diff is limited to lazy-loading heavy optional dependencies and plain coordinate type changes.
- Step 2: Run final verification
Run:
cd web && pnpm lint && pnpm build
Expected: PASS.
- Step 3: Commit
Run:
git add web/src/main.tsx web/src/components/map web/src/components/MemoEditor web/src/components/MemoMetadata/Location web/src/pages/UserProfile.tsx web/src/components/MemoContent web/src/components/UserMemoMap/UserMemoMap.tsx
git commit -m "perf: lazy load heavy first-screen dependencies"
Expected: Commit succeeds with only the intended source changes.
Self-Review
- Spec coverage: The plan removes global Leaflet/KaTeX CSS, dynamically imports Mermaid, lazy-loads map UI, keeps Leaflet types out of parent boundaries, and verifies auth/signup network behavior.
- Placeholder scan: No placeholder markers, unresolved decisions, or vague generic handling steps remain.
- Type consistency:
MapPointis the shared parent-facing coordinate type;LatLngremains internal toLocationPickerand map implementation files only.