了解如何诊断和解决 @feoe/fs-router 相关的性能问题。
使用浏览器开发者工具
1// 在控制台中测量路由切换时间
2console.time('route-change')
3navigate('/new-route')
4// 在新页面加载完成后
5console.timeEnd('route-change')
React Profiler
1import { Profiler } from 'react'
2
3function onRenderCallback(id, phase, actualDuration) {
4 console.log('组件渲染时间:', { id, phase, actualDuration })
5}
6
7<Profiler id="App" onRender={onRenderCallback}>
8 <RouterProvider router={router} />
9</Profiler>
网络面板分析
1// 自定义性能监控 Hook
2function usePerformanceMonitor() {
3 const location = useLocation()
4
5 useEffect(() => {
6 const startTime = performance.now()
7
8 return () => {
9 const endTime = performance.now()
10 const duration = endTime - startTime
11
12 // 记录页面停留时间
13 console.log(`页面 ${location.pathname} 停留时间: ${duration}ms`)
14
15 // 发送到分析服务
16 analytics.track('page_duration', {
17 path: location.pathname,
18 duration
19 })
20 }
21 }, [location])
22}
启用代码分割
1// vite.config.ts
2export default defineConfig({
3 plugins: [
4 FileBasedRouterVite({
5 typeGenerateOptions: {
6 routesTypeFile: 'src/routes-type.ts',
7 generateRouteParams: true,
8 generateLoaderTypes: true,
9 routesDirectories: []
10 }
11 })
12 ],
13 build: {
14 rollupOptions: {
15 output: {
16 manualChunks: {
17 vendor: ['react', 'react-dom', 'react-router-dom'],
18 ui: ['@mui/material', 'antd'] // UI 库单独打包
19 }
20 }
21 }
22 }
23})
路由级别的代码分割
1// 自动生成的懒加载组件
2const HomePage = lazy(() => import('./routes/page'))
3const AboutPage = lazy(() => import('./routes/about/page'))
4const UserPage = lazy(() => import('./routes/user/[id]/page'))
预加载关键路由
1// 在应用启动时预加载重要路由
2const preloadRoutes = [
3 () => import('./routes/page'),
4 () => import('./routes/about/page')
5]
6
7// 在空闲时间预加载
8if ('requestIdleCallback' in window) {
9 requestIdleCallback(() => {
10 preloadRoutes.forEach(load => load())
11 })
12}
使用 Resource Hints
1<!-- 在 index.html 中添加 -->
2<link rel="preload" href="/src/routes.tsx" as="script">
3<link rel="prefetch" href="/src/routes/about/page.tsx" as="script">
配置 Webpack/Vite 预加载
1// vite.config.ts
2export default defineConfig({
3 build: {
4 rollupOptions: {
5 output: {
6 chunkFileNames: (chunkInfo) => {
7 // 为路由 chunk 添加预加载提示
8 if (chunkInfo.name?.includes('routes')) {
9 return 'routes/[name]-[hash].js'
10 }
11 return '[name]-[hash].js'
12 }
13 }
14 }
15 }
16})
智能预加载
1// 鼠标悬停时预加载
2function PreloadLink({ to, children, ...props }) {
3 const [isHovered, setIsHovered] = useState(false)
4
5 useEffect(() => {
6 if (isHovered) {
7 // 预加载目标路由
8 import(`./routes${to}/page`)
9 }
10 }, [isHovered, to])
11
12 return (
13 <Link
14 to={to}
15 onMouseEnter={() => setIsHovered(true)}
16 {...props}
17 >
18 {children}
19 </Link>
20 )
21}
视口预加载
1// 当链接进入视口时预加载
2function useIntersectionPreload(ref, importFn) {
3 useEffect(() => {
4 const observer = new IntersectionObserver(
5 ([entry]) => {
6 if (entry.isIntersecting) {
7 importFn()
8 observer.disconnect()
9 }
10 },
11 { threshold: 0.1 }
12 )
13
14 if (ref.current) {
15 observer.observe(ref.current)
16 }
17
18 return () => observer.disconnect()
19 }, [importFn])
20}
分层 Suspense
1function App() {
2 return (
3 <Router>
4 <Suspense fallback={<AppSkeleton />}>
5 <Layout>
6 <Suspense fallback={<PageSkeleton />}>
7 <Routes />
8 </Suspense>
9 </Layout>
10 </Suspense>
11 </Router>
12 )
13}
智能 Loading 状态
1function SmartSuspense({ children, fallback }) {
2 const [showFallback, setShowFallback] = useState(false)
3
4 useEffect(() => {
5 const timer = setTimeout(() => {
6 setShowFallback(true)
7 }, 200) // 200ms 后才显示 loading
8
9 return () => clearTimeout(timer)
10 }, [])
11
12 return (
13 <Suspense fallback={showFallback ? fallback : null}>
14 {children}
15 </Suspense>
16 )
17}
并行数据加载
1export async function loader({ params }) {
2 // 并行加载多个数据源
3 const [user, posts, comments] = await Promise.all([
4 fetchUser(params.id),
5 fetchUserPosts(params.id),
6 fetchUserComments(params.id)
7 ])
8
9 return { user, posts, comments }
10}
数据缓存策略
1// 简单的内存缓存
2const cache = new Map()
3const CACHE_TTL = 5 * 60 * 1000 // 5分钟
4
5export async function loader({ params }) {
6 const cacheKey = `user-${params.id}`
7 const cached = cache.get(cacheKey)
8
9 if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
10 return cached.data
11 }
12
13 const user = await fetchUser(params.id)
14 cache.set(cacheKey, {
15 data: { user },
16 timestamp: Date.now()
17 })
18
19 return { user }
20}
增量数据加载
1export async function loader({ params, request }) {
2 const url = new URL(request.url)
3 const page = parseInt(url.searchParams.get('page') || '1')
4 const pageSize = 20
5
6 // 只加载当前页数据
7 const posts = await fetchPosts({
8 userId: params.id,
9 page,
10 pageSize
11 })
12
13 return { posts, page, hasMore: posts.length === pageSize }
14}
1import { useQuery } from '@tanstack/react-query'
2
3export async function loader({ params }) {
4 // 在 loader 中预填充查询缓存
5 const queryClient = getQueryClient()
6
7 await queryClient.prefetchQuery({
8 queryKey: ['user', params.id],
9 queryFn: () => fetchUser(params.id)
10 })
11
12 return null // 数据通过 React Query 管理
13}
14
15export default function UserPage() {
16 const { id } = useParams()
17 const { data: user } = useQuery({
18 queryKey: ['user', id],
19 queryFn: () => fetchUser(id)
20 })
21
22 return <div>{user?.name}</div>
23}
清理事件监听器
1export default function UserPage() {
2 useEffect(() => {
3 const handleResize = () => {
4 // 处理窗口大小变化
5 }
6
7 window.addEventListener('resize', handleResize)
8
9 return () => {
10 window.removeEventListener('resize', handleResize)
11 }
12 }, [])
13}
清理定时器
1export default function AutoRefreshPage() {
2 useEffect(() => {
3 const interval = setInterval(() => {
4 // 自动刷新数据
5 }, 30000)
6
7 return () => clearInterval(interval)
8 }, [])
9}
取消未完成的请求
1export async function loader({ params, signal }) {
2 try {
3 const user = await fetchUser(params.id, { signal })
4 return { user }
5 } catch (error) {
6 if (error.name === 'AbortError') {
7 // 请求被取消,不需要处理
8 return null
9 }
10 throw error
11 }
12}
使用 React.memo
1const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
2 // 复杂的渲染逻辑
3 return <div>{/* 渲染内容 */}</div>
4})
优化依赖数组
1// ❌ 避免
2useEffect(() => {
3 fetchData(user)
4}, [user]) // user 对象每次都不同
5
6// ✅ 推荐
7useEffect(() => {
8 fetchData(user)
9}, [user.id]) // 只依赖必要的属性
分析 Bundle 大小
1# 使用 webpack-bundle-analyzer
2npm install --save-dev webpack-bundle-analyzer
3
4# 或使用 Vite 插件
5npm install --save-dev rollup-plugin-visualizer
配置分析工具
1// vite.config.ts
2import { visualizer } from 'rollup-plugin-visualizer'
3
4export default defineConfig({
5 plugins: [
6 // ... 其他插件
7 visualizer({
8 filename: 'dist/stats.html',
9 open: true
10 })
11 ]
12})
确保正确的导入方式
1// ❌ 避免 - 导入整个库
2import * as _ from 'lodash'
3
4// ✅ 推荐 - 只导入需要的函数
5import { debounce } from 'lodash'
6
7// 或使用具体路径
8import debounce from 'lodash/debounce'
配置 sideEffects
1// package.json
2{
3 "sideEffects": false
4}
1// 性能监控服务
2class PerformanceMonitor {
3 static measureRouteChange(from: string, to: string) {
4 const startTime = performance.now()
5
6 return () => {
7 const endTime = performance.now()
8 const duration = endTime - startTime
9
10 // 发送到分析服务
11 this.sendMetric('route_change_duration', {
12 from,
13 to,
14 duration
15 })
16 }
17 }
18
19 static measureComponentRender(componentName: string) {
20 return (id: string, phase: string, actualDuration: number) => {
21 if (actualDuration > 16) { // 超过一帧的时间
22 this.sendMetric('slow_component_render', {
23 component: componentName,
24 phase,
25 duration: actualDuration
26 })
27 }
28 }
29 }
30
31 private static sendMetric(name: string, data: any) {
32 // 发送到分析服务
33 if (typeof window !== 'undefined' && window.gtag) {
34 window.gtag('event', name, data)
35 }
36 }
37}
38
39// 在组件中使用
40export default function UserPage() {
41 const location = useLocation()
42
43 useEffect(() => {
44 const cleanup = PerformanceMonitor.measureRouteChange(
45 document.referrer,
46 location.pathname
47 )
48
49 return cleanup
50 }, [location])
51
52 return (
53 <Profiler
54 id="UserPage"
55 onRender={PerformanceMonitor.measureComponentRender('UserPage')}
56 >
57 {/* 页面内容 */}
58 </Profiler>
59 )
60}
1// 监控 Core Web Vitals
2import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'
3
4function sendToAnalytics(metric) {
5 // 发送到分析服务
6 gtag('event', metric.name, {
7 value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
8 event_category: 'Web Vitals',
9 event_label: metric.id,
10 non_interaction: true
11 })
12}
13
14// 测量所有指标
15getCLS(sendToAnalytics)
16getFID(sendToAnalytics)
17getFCP(sendToAnalytics)
18getLCP(sendToAnalytics)
19getTTFB(sendToAnalytics)