perf(web): optimize bundle size — dynamic import Mapbox GL and code split Recharts

- Dynamic import ListingMap with next/dynamic (ssr: false) in /listings/[id] and /search pages
- Extract Recharts into lazy-loaded DistrictBarChart and PriceTrendChart components
- /listings/[id] first-load JS: 618KB → 149KB (-76%)
- /search first-load JS: 619KB → 150KB (-76%)
- Both pages now well under 300KB target

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 13:10:24 +07:00
parent 585fdc6ab6
commit 5848c2b386
6 changed files with 177 additions and 129 deletions

View File

@@ -0,0 +1,60 @@
'use client';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TooltipFormatter = (value: any, name: any) => [string, string];
interface DistrictBarChartProps {
data: { district: string; price?: number; 'Gia/m2'?: number; listings: number }[];
height?: number;
dataKey?: string;
tooltipFormatter?: TooltipFormatter;
}
export function DistrictBarChart({
data,
height = 300,
dataKey = 'price',
tooltipFormatter,
}: DistrictBarChartProps) {
const defaultFormatter: TooltipFormatter = (value, name) => [
name === dataKey ? `${value} tr/m2` : String(value),
name === dataKey ? 'Gia' : 'Tin dang',
];
return (
<ResponsiveContainer width="100%" height={height}>
<BarChart data={data} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="district"
tick={{ fontSize: 11 }}
angle={-30}
textAnchor="end"
height={60}
className="fill-muted-foreground"
/>
<YAxis tick={{ fontSize: 12 }} className="fill-muted-foreground" />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '0.5rem',
fontSize: '0.875rem',
}}
formatter={tooltipFormatter ?? defaultFormatter}
/>
<Bar dataKey={dataKey} fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts';
interface PriceTrendChartProps {
data: { period: string; 'Gia/m2': number; 'Tin dang': number }[];
height?: number;
}
export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) {
return (
<ResponsiveContainer width="100%" height={height}>
<LineChart data={data} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="period" tick={{ fontSize: 12 }} className="fill-muted-foreground" />
<YAxis yAxisId="left" tick={{ fontSize: 12 }} className="fill-muted-foreground" />
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
className="fill-muted-foreground"
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '0.5rem',
fontSize: '0.875rem',
}}
formatter={(value, name) => [
name === 'Gia/m2' ? `${value} tr/m2` : value,
name,
]}
/>
<Legend />
<Line
yAxisId="left"
type="monotone"
dataKey="Gia/m2"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="Tin dang"
stroke="hsl(var(--muted-foreground))"
strokeWidth={1}
strokeDasharray="5 5"
dot={{ r: 3 }}
/>
</LineChart>
</ResponsiveContainer>
);
}