Next.js 动态导入详解:解决图表库 SSR 兼容问题

chat

问题背景

在 Next.js 项目中使用 @ant-design/chartsechartsreact-wordcloud 等图表库时,经常会遇到以下错误:

ReferenceError: window is not defined
// 或
Cannot read properties of undefined (reading '0')

这是因为这些库依赖浏览器 API(windowdocument、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 不匹配

注意事项

  1. loading 状态:建议提供 loading 占位符,避免布局跳动
  2. 高度固定:动态加载的组件最好设置固定高度,防止 CLS(布局偏移)
  3. 'use client' 指令:在 App Router 中,使用动态导入的组件需要标记为客户端组件
  4. 性能考虑:动态导入会增加一次网络请求,但对于大型图表库来说,这种按需加载反而能优化首屏性能

总结

在 Next.js 中使用依赖浏览器 API 的图表库时,必须使用动态导入并禁用 SSR。这是 Next.js 官方推荐的最佳实践,既能解决 SSR 兼容问题,又能实现按需加载优化性能。


版权声明:
作者:东明兄
链接:https://blog.crazyming.com/note/3284/
来源:CrazyMing
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
Next.js 动态导入详解:解决图表库 SSR 兼容问题
问题背景 在 Next.js 项目中使用 @ant-design/charts、echarts、react-wordcloud 等图表库时,经常会遇到以下错误: ReferenceError: window is not defined /……
<<上一篇
下一篇>>
chat