Home / Blog / Why Map.tsx is fourteen lines
May 11, 2026· Wine World Map
Why Map.tsx is fourteen lines
The file components/Map.tsx is the entry point for the entire
interactive map. Here it is in its entirety:
'use client'
import dynamic from 'next/dynamic'
const MapComponent = dynamic(() => import('./MapComponent'), {
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center bg-[#0d0407]">
<div className="text-[#c9a84c] font-serif text-lg animate-pulse">Laddar karta…</div>
</div>
),
})
export default MapComponent
Fourteen lines, of which seven are the loading spinner. The actual
map — MapComponent.tsx — is 534 lines: marker clustering, layer
switching, click handlers, flyTo animations, a pulsing-ring
highlight for search results. None of that ships with Map.tsx
itself.
The reason is the seven-letter argument ssr: false.
Why Leaflet can't be server-rendered
Leaflet, the library that draws the actual map, touches window
the moment it's imported:
// from leaflet's source, paraphrased
L.Browser = (function () {
const ua = navigator.userAgent.toLowerCase()
// ...feature detection
})()
That navigator.userAgent line runs at module-evaluation time, not
inside a function. So import 'leaflet' is enough to throw
ReferenceError: navigator is not defined in a Node.js
environment — which is exactly where Next.js renders Server
Components.
You can't lazy-import inside MapComponent.tsx either, because the
component file itself has import 'leaflet' at the top. The
import has to be moved out of any code path that runs on the
server, period.
What dynamic(..., { ssr: false }) actually does
It splits the bundle. MapComponent.tsx and everything it imports
(Leaflet, markercluster, the CSS) gets emitted as a separate JS
chunk that's only fetched client-side. The server renders the
loading fallback. Hydration on the client then triggers the
chunk load, and once it arrives, React swaps the spinner for the
map.
The bundle-split is also why this is desirable even when SSR could work: Leaflet plus markercluster plus their CSS is roughly 160 KB compressed. Sites that don't need the map (most blog pageviews) never download it.
The cost
Two things suffer:
-
First-paint shows the spinner, not the map. On a fast connection this is ~200 ms. On 3G it's a couple of seconds. The alternative is a server-rendered placeholder that looks like a map but isn't, which is worse — visitors click it and nothing happens.
-
Search engines don't see the map. Googlebot does a JS-aware crawl these days, but it's not guaranteed to wait for the dynamic chunk to load. For the
/maproute this is fine because the SEO value lives in the per-region landing pages, which are server-rendered.
The 'use client' line is doing real work
That 'use client' at the top of the file is the seam between
Server Components (the default in Next.js App Router) and Client
Components. Everything imported from a Client Component is also a
client boundary. The dynamic() call only works in a Client
Component, because it relies on React.lazy semantics.
If we removed 'use client', the import of next/dynamic would
fail to typecheck. If we kept 'use client' but inlined the
Leaflet import here instead of through dynamic, the server build
would crash at compile time. The fourteen lines are load-bearing in
both directions.
When the fourteen-line file pays off
Every time we change something inside the map — adding a layer,
tuning the cluster radius, fixing a bug in flyTo — we touch
MapComponent.tsx. We never touch Map.tsx. The boundary stays
stable. The 534-line file gets edited weekly; the 14-line file
gets edited approximately never.
That's the unglamorous design payoff of putting a thin shim around a heavy dependency: the shim absorbs the SSR weirdness, and the heavy file gets to be a plain old client component that imports whatever it wants.
#nextjs#leaflet#ssr