Next.js 动态导入详解:解决图表库 SSR 兼容问题
问题背景
在 Next.js 项目中使用 @ant-design/charts、echarts、react-wordcloud 等图表库时,经常会遇到以下错误:
ReferenceError: window is not defined
// 或
Cannot read properties of undefined (reading '0')
这是因为这些库依赖浏览器 API(window、document、Canvas),而 Next.js 的 SSR 阶段运行在 Node.js 环境中,这些 API 不存在。
Next.js 渲染流程
下图展示了 Next.js 的渲染流程,以及为什么普通 import 会导致问题:
flowchart TB
subgraph SSR["服务端渲染 (Node.js)"]
A[页面请求] --> B[执行 import 语句]
B --> C{是否依赖浏览器 API?}
C -->|是| D[❌ 报错: window is not defined]
C -->|否| E[✅ 正常渲染 HTML]
end
subgraph CSR["客户端渲染 (浏览器)"]
F[接收 HTML] --> G[Hydration 水合]
G --> H[执行客户端代码]
H --> I[✅ 浏览器 API 可用]
end
E --> F
普通的 import 语句会在两端都执行,导致 SSR 阶段崩溃。
解决方案:动态导入 + 禁用 SSR
使用 Next.js 提供的 dynamic 函数,配合 ssr: false 选项:
基础用法
import dynamic from 'next/dynamic';
// 动态导入,禁用 SSR
const WordCloud = dynamic(
() => import('@ant-design/charts').then((mod) => mod.WordCloud),
{
ssr: false,
loading: () => (
<div className="flex items-center justify-center h-[200px]">
加载中...
</div>
),
}
);
工作原理
sequenceDiagram
participant Server as 服务端 (Node.js)
participant Browser as 浏览器
participant Component as 动态组件
Note over Server: SSR 阶段
Server->>Server: 遇到 dynamic 组件
Server->>Server: 渲染 loading 占位符
Server-->>Browser: 返回 HTML (含占位符)
Note over Browser: CSR 阶段
Browser->>Browser: 页面加载完成
Browser->>Component: 动态 import 组件
Component-->>Browser: 组件代码加载完成
Browser->>Browser: 替换占位符为真实组件
| 阶段 | 行为 |
|---|---|
| SSR 阶段 | 渲染 loading 占位符,不执行 import |
| CSR 阶段 | 浏览器加载完成后,动态加载真实组件 |
实战案例:标签云组件
以下是一个完整的标签云组件示例,使用 @ant-design/charts 的 WordCloud:
'use client';
import React, { useMemo } from 'react';
import dynamic from 'next/dynamic';
// 动态导入词云组件
const WordCloud = dynamic(
() => import('@ant-design/charts').then((mod) => mod.WordCloud),
{
ssr: false,
loading: () => (
<div className="flex items-center justify-center h-[220px] text-gray-400">
加载中...
</div>
),
}
);
interface Tag {
id: number;
name: string;
color: string;
usageCount: number;
}
interface TagCloudProps {
tags: Tag[];
onTagClick?: (tag: Tag) => void;
}
export default function TagCloud({ tags, onTagClick }: TagCloudProps) {
// 转换数据格式
const wordCloudData = useMemo(() => {
return tags.map((tag) => ({
text: tag.name,
value: Math.max(tag.usageCount || 1, 1),
id: tag.id,
color: tag.color,
}));
}, [tags]);
// 词云配置
const config = useMemo(
() => ({
data: wordCloudData,
wordField: 'text',
weightField: 'value',
height: 220,
wordStyle: {
fontSize: [16, 56] as [number, number],
rotation: 0,
},
color: (_: string, __: number, datum: any) => datum.color,
tooltip: {
formatter: (datum: any) => ({
name: datum.text,
value: `${datum.value} 次使用`,
}),
},
onReady: (plot: any) => {
plot.on('element:click', (evt: any) => {
const data = evt?.data?.data;
if (data && onTagClick) {
const tag = tags.find((t) => t.id === data.id);
if (tag) onTagClick(tag);
}
});
},
}),
[wordCloudData, tags, onTagClick]
);
if (tags.length === 0) {
return (
<div className="flex items-center justify-center h-[220px] text-gray-400">
暂无标签
</div>
);
}
return <WordCloud {...config} />;
}
其他常见图表库的动态导入
ECharts
const ReactECharts = dynamic(
() => import('echarts-for-react'),
{ ssr: false }
);
React-Wordcloud
const ReactWordcloud = dynamic(
() => import('react-wordcloud'),
{ ssr: false }
);
Chart.js
const Line = dynamic(
() => import('react-chartjs-2').then((mod) => mod.Line),
{ ssr: false }
);
方案对比
| 方案 | SSR 兼容 | 说明 |
|---|---|---|
| 普通 import | ❌ | 服务端报错 |
| dynamic + ssr: false | ✅ | 只在客户端加载 |
| typeof window 判断 | ⚠️ | 可能导致 hydration 不匹配 |
注意事项
- loading 状态:建议提供 loading 占位符,避免布局跳动
- 高度固定:动态加载的组件最好设置固定高度,防止 CLS(布局偏移)
- 'use client' 指令:在 App Router 中,使用动态导入的组件需要标记为客户端组件
- 性能考虑:动态导入会增加一次网络请求,但对于大型图表库来说,这种按需加载反而能优化首屏性能
总结
在 Next.js 中使用依赖浏览器 API 的图表库时,必须使用动态导入并禁用 SSR。这是 Next.js 官方推荐的最佳实践,既能解决 SSR 兼容问题,又能实现按需加载优化性能。
版权声明:
作者:东明兄
链接:https://blog.crazyming.com/note/3284/
来源:CrazyMing
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论