From 5848c2b386445f87488dd5564d397c40e9d90ae2 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 8 Apr 2026 13:10:24 +0700 Subject: [PATCH] =?UTF-8?q?perf(web):=20optimize=20bundle=20size=20?= =?UTF-8?q?=E2=80=94=20dynamic=20import=20Mapbox=20GL=20and=20code=20split?= =?UTF-8?q?=20Recharts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/app/(dashboard)/analytics/page.tsx | 110 +++--------------- apps/web/app/(dashboard)/dashboard/page.tsx | 42 ++----- apps/web/app/(public)/listings/[id]/page.tsx | 14 ++- apps/web/app/(public)/search/page.tsx | 14 ++- .../components/charts/district-bar-chart.tsx | 60 ++++++++++ .../components/charts/price-trend-chart.tsx | 66 +++++++++++ 6 files changed, 177 insertions(+), 129 deletions(-) create mode 100644 apps/web/components/charts/district-bar-chart.tsx create mode 100644 apps/web/components/charts/price-trend-chart.tsx diff --git a/apps/web/app/(dashboard)/analytics/page.tsx b/apps/web/app/(dashboard)/analytics/page.tsx index d14f045..8e2436c 100644 --- a/apps/web/app/(dashboard)/analytics/page.tsx +++ b/apps/web/app/(dashboard)/analytics/page.tsx @@ -1,18 +1,7 @@ 'use client'; +import dynamic from 'next/dynamic'; import { useEffect, useState } from 'react'; -import { - BarChart, - Bar, - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - Legend, -} from 'recharts'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; @@ -24,6 +13,16 @@ import { type PriceTrendPoint, } from '@/lib/analytics-api'; +const DistrictBarChart = dynamic( + () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), + { ssr: false, loading: () =>
Dang tai bieu do...
}, +); + +const PriceTrendChart = dynamic( + () => import('@/components/charts/price-trend-chart').then((mod) => mod.PriceTrendChart), + { ssr: false, loading: () =>
Dang tai bieu do...
}, +); + const CITIES = ['Ho Chi Minh', 'Ha Noi', 'Da Nang']; const CURRENT_PERIOD = '2026-Q1'; const TREND_PERIODS = ['2025-Q1', '2025-Q2', '2025-Q3', '2025-Q4', '2026-Q1']; @@ -223,36 +222,7 @@ export default function AnalyticsPage() { Chua co du lieu ) : ( - - - - - - [ - name === 'price' ? `${value} tr/m2` : value, - name === 'price' ? 'Gia' : 'Tin dang', - ]} - /> - - - + )} @@ -345,61 +315,7 @@ export default function AnalyticsPage() { Chua co du lieu xu huong ) : ( - - - - - - - [ - name === 'Gia/m2' ? `${value} tr/m2` : value, - name, - ]} - /> - - - - - + )} diff --git a/apps/web/app/(dashboard)/dashboard/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx index 4f731ec..ef80e3d 100644 --- a/apps/web/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/page.tsx @@ -1,17 +1,9 @@ 'use client'; +import dynamic from 'next/dynamic'; import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useState } from 'react'; -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from 'recharts'; import { ListingStatusBadge } from '@/components/listings/listing-status-badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -22,6 +14,11 @@ import { } from '@/lib/analytics-api'; import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api'; +const DistrictBarChart = dynamic( + () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), + { ssr: false, loading: () =>
Dang tai bieu do...
}, +); + const CITY = 'Ho Chi Minh'; const PERIOD = '2026-Q1'; @@ -180,27 +177,12 @@ export default function DashboardPage() { Chua co du lieu ) : ( - - - - - - [`${value} tr/m2`, 'Gia']} - /> - - - + [`${value} tr/m2`, 'Gia']} + /> )} diff --git a/apps/web/app/(public)/listings/[id]/page.tsx b/apps/web/app/(public)/listings/[id]/page.tsx index 1a8f609..d153d7a 100644 --- a/apps/web/app/(public)/listings/[id]/page.tsx +++ b/apps/web/app/(public)/listings/[id]/page.tsx @@ -1,16 +1,28 @@ 'use client'; +import dynamic from 'next/dynamic'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import * as React from 'react'; import { ImageGallery } from '@/components/listings/image-gallery'; -import { ListingMap } from '@/components/map/listing-map'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { listingsApi, type ListingDetail } from '@/lib/listings-api'; import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings'; +const ListingMap = dynamic( + () => import('@/components/map/listing-map').then((mod) => mod.ListingMap), + { + ssr: false, + loading: () => ( +
+

Dang tai ban do...

+
+ ), + }, +); + function formatPrice(priceVND: string): string { const num = Number(priceVND); if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`; diff --git a/apps/web/app/(public)/search/page.tsx b/apps/web/app/(public)/search/page.tsx index e3f6e35..2310b3c 100644 --- a/apps/web/app/(public)/search/page.tsx +++ b/apps/web/app/(public)/search/page.tsx @@ -1,13 +1,25 @@ 'use client'; +import dynamic from 'next/dynamic'; import { useRouter, useSearchParams } from 'next/navigation'; import * as React from 'react'; -import { ListingMap } from '@/components/map/listing-map'; import { FilterBar, type SearchFilters } from '@/components/search/filter-bar'; import { SearchResults } from '@/components/search/search-results'; import { Button } from '@/components/ui/button'; import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api'; +const ListingMap = dynamic( + () => import('@/components/map/listing-map').then((mod) => mod.ListingMap), + { + ssr: false, + loading: () => ( +
+

Dang tai ban do...

+
+ ), + }, +); + type ViewMode = 'list' | 'map' | 'split'; const defaultFilters: SearchFilters = { diff --git a/apps/web/components/charts/district-bar-chart.tsx b/apps/web/components/charts/district-bar-chart.tsx new file mode 100644 index 0000000..833fc11 --- /dev/null +++ b/apps/web/components/charts/district-bar-chart.tsx @@ -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 ( + + + + + + + + + + ); +} diff --git a/apps/web/components/charts/price-trend-chart.tsx b/apps/web/components/charts/price-trend-chart.tsx new file mode 100644 index 0000000..afe9ee7 --- /dev/null +++ b/apps/web/components/charts/price-trend-chart.tsx @@ -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 ( + + + + + + + [ + name === 'Gia/m2' ? `${value} tr/m2` : value, + name, + ]} + /> + + + + + + ); +}