You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js14+Highcharts多图表渲染触发message handler性能警告求助

问题

我正在使用Next.js 14结合Highcharts在同一页面渲染3-5个图表,数据集并不复杂,但页面切换或多图表同时渲染时会出现性能警告:

[Violation] 'message' handler took xxms

项目配置

  • 采用Next.js 14 App Router
  • 用ssr: false的动态导入实现图表客户端懒加载
  • 复用BaseChart组件渲染折线图、柱状图等不同类型图表

核心代码概述

BaseGraph.tsx

import React, { useEffect, useRef } from 'react';
import Highcharts, { type SeriesOptionsType } from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import AccessibilityModule from 'highcharts/modules/accessibility';
import useMessages from '@/services/hooks/useMessages';
import NoDataToDisplay from 'highcharts/modules/no-data-to-display';
import { ChartBreakPoints, ChartTypes } from '@/services/charts/ChartService';
import { Colors } from '@/utils/colors';
import HighchartsMap from 'highcharts/modules/map';
import { merge } from '@/utils/Highcharts';

interface BaseChartProps {
  chartType: string
  chartData: any
  chartOptions: Highcharts.Options
  isLoading: boolean
  setIsLoading: (isLoading: boolean) => void
  yAxisUnit?: string
  chartId: string
  title?: string
  isForHomePage?: boolean
}

export default function BaseChart ({
  chartType,
  chartData,
  isLoading,
  setIsLoading,
  chartOptions,
  yAxisUnit,
  chartId,
  title,
  isForHomePage = false
}: Readonly<BaseChartProps>): React.JSX.Element {
  const messages = useMessages();
  const chartRef = useRef<HighchartsReact.RefObject>(null);
  const chart = chartRef.current?.chart as any;

  useEffect(() => {
    NoDataToDisplay(Highcharts);
    AccessibilityModule(Highcharts);
    HighchartsMap(Highcharts);

    Highcharts.setOptions({
      lang: {
        noData: messages('charts.no-data-message'),
        decimalPoint: ','
      }
    });
  }, []);

  const commonOptions: Highcharts.Options = {
    chart: {
      type: chartType,
      animation: false,
    },
    noData: {
      style: {
        fontWeight: '600',
        fontSize: '16px',
        color: '#0D0D0D'
      }
    },
    tooltip: {
      xDateFormat: '%d/%m/%Y',
      borderRadius: 8,
      shadow: {
        color: '#000000',
        opacity: 0.02,
        offsetX: 0,
        offsetY: 2,
        width: 10
      },
      shared: false,
      useHTML: true,
    },
    legend: {
      itemHoverStyle: {
        color: '#0D0D0D'
      },
      alignColumns: false,
    },
    plotOptions: {
      series: {
        events: {
          legendItemClick: function (e) {
            e.preventDefault();
          }
        },
        animation: false
      }
    },
 
    series: chartData as SeriesOptionsType[]
  };

  const finalOptions: Highcharts.Options = merge(commonOptions, chartOptions);

  return (
    <section id={chartId}>
      <HighchartsReact
        highcharts={Highcharts}
        options={finalOptions}
        ref={chartRef}
        constructorType={chartType === ChartTypes.MAP ? 'mapChart' : undefined}
      />
    </section>
  );
}

LineChart.tsx

'use client';
import React from 'react';
import type Highcharts from 'highcharts';
import ReactDOMServer from 'react-dom/server';
import LineChartTooltip from './LineChartTooltip';
import { type LineChartEvolDataType } from '@/services/charts/types/ChartDataTypes';
import { ChartTypes } from '@/services/charts/ChartService';
import LazyBaseChart from '@/components/charts/common/LazyBaseChart';

interface Props {
  lineChartData: LineChartEvolDataType
  yAxisUnit: string
  tooltipUnit?: string
  chartId: string
  isForHomePage?: boolean
  min?: number
  max?: number
  title?: string
  isLoading: boolean
  setIsLoading: (loading: boolean) => void
  numberDecimals?: number
}
export default function LineChart ({
  lineChartData,
  yAxisUnit,
  isForHomePage = false,
  numberDecimals,
  min,
  max,
  chartId,
  isLoading,
  tooltipUnit,
  setIsLoading,
  title
}: Readonly<Props>): React.JSX.Element {
  const options: Highcharts.Options = {
    tooltip: {
      shared: true,
      shape: 'rect',
      formatter ({ chart }) {
        const points = chart.hoverPoints;
        if (points?.[0].x) {
          const element = React.createElement(LineChartTooltip, {
            points,
            x: parseFloat(points[0]?.x.toString()),
            yAxisUnit: tooltipUnit ?? yAxisUnit,
            numberDecimals
          });
          return ReactDOMServer.renderToString(element);
        }
      }
    }
  };

  return (
    <LazyBaseChart
      chartType={ChartTypes.LINE}
      chartData={lineChartData}
      isLoading={isLoading}
      setIsLoading={setIsLoading}
      chartOptions={options}
      chartId={chartId}
      yAxisUnit={yAxisUnit}
      title={title}
      isForHomePage={isForHomePage}
    />
  );
}

LazyBaseChart.tsx

import dynamic from 'next/dynamic';
import Loader from '@/components/theme/atoms/Loader';
import React from 'react';

const LazyBaseChart = dynamic(
  async () => await import('@/components/charts/common/BaseGraph'), {
    ssr: false,
    loading: () => <Loader />
  });

export default LazyBaseChart;

我已尝试移除动画、禁用部分Highcharts模块,但警告仍存在,仅在多图表渲染时出现,单个图表无问题,求分析与优化建议。


分析与优化建议

警告原因

这个message handler超时警告是浏览器提示主线程被长时间同步任务阻塞(超过50ms),多图表同时渲染时,多个Highcharts实例的初始化、DOM操作、模块加载等操作叠加,导致事件循环无法及时处理其他消息。

具体优化方案

1. 全局初始化Highcharts模块,避免重复执行

当前BaseChart的useEffect会在每个图表挂载时重复加载NoDataToDisplay、AccessibilityModule等模块,多图表场景下造成冗余开销。

修改方式:
将模块初始化移到全局入口(比如app/layout.tsx或单独的init-highcharts.ts),仅执行一次:

// app/init-highcharts.ts
import Highcharts from 'highcharts';
import AccessibilityModule from 'highcharts/modules/accessibility';
import NoDataToDisplay from 'highcharts/modules/no-data-to-display';
import HighchartsMap from 'highcharts/modules/map';

// 全局加载模块
NoDataToDisplay(Highcharts);
AccessibilityModule(Highcharts);
HighchartsMap(Highcharts);

// 全局语言配置(客户端组件中调用)
export const initHighchartsLang = (noDataMsg: string) => {
  Highcharts.setOptions({
    lang: {
      noData: noDataMsg,
      decimalPoint: ','
    }
  });
};

然后在客户端根组件(如app/layout.tsx的客户端部分)调用initHighchartsLang,同时删除BaseChart中对应的useEffect代码。

2. 缓存图表配置,避免不必要重渲染

当前commonOptions和finalOptions每次渲染都会重新创建对象,导致HighchartsReact频繁触发更新,用useMemo缓存这些配置:

// BaseChart.tsx 中导入useMemo
import React, { useEffect, useRef, useMemo } from 'react';

// ...

const commonOptions: Highcharts.Options = useMemo(() => ({
  chart: {
    type: chartType,
    animation: false,
  },
  noData: {
    style: {
      fontWeight: '600',
      fontSize: '16px',
      color: '#0D0D0D'
    }
  },
  tooltip: {
    xDateFormat: '%d/%m/%Y',
    borderRadius: 8,
    shadow: {
      color: '#000000',
      opacity: 0.02,
      offsetX: 0,
      offsetY: 2,
      width: 10
    },
    shared: false,
    useHTML: true,
  },
  legend: {
    itemHoverStyle: {
      color: '#0D0D0D'
    },
    alignColumns: false,
  },
  plotOptions: {
    series: {
      events: {
        legendItemClick: function (e) {
          e.preventDefault();
        }
      },
      animation: false
    }
  },
  series: chartData as SeriesOptionsType[]
}), [chartType, chartData]);

const finalOptions: Highcharts.Options = useMemo(() => 
  merge(commonOptions, chartOptions), 
[commonOptions, chartOptions]);

3. 优化Tooltip渲染,移除同步DOM字符串转换

LineChart中ReactDOMServer.renderToString是同步阻塞操作,多图表同时触发时会加重主线程负担,改用原生HTML字符串拼接:

// LineChart.tsx 中修改tooltip formatter
formatter ({ points }) {
  if (!points?.length) return '';
  const x = parseFloat(points[0]?.x.toString());
  const unit = tooltipUnit ?? yAxisUnit;
  const decimals = numberDecimals ?? 0;
  
  let tooltipHtml = `<div class="line-chart-tooltip"><strong>${new Date(x).toLocaleDateString()}</strong>`;
  points.forEach(point => {
    tooltipHtml += `<div><span style="color: ${point.color}">●</span> ${point.series.name}: ${point.y.toFixed(decimals)} ${unit}</div>`;
  });
  tooltipHtml += '</div>';
  return tooltipHtml;
}

4. 分批渲染图表,分散主线程压力

多图表同时初始化会瞬间占满主线程,可通过分批延迟加载或视口触发加载分散压力:

  • 分批延迟加载:在父组件中给每个图表设置不同的加载延迟:
// 父组件示例
const [charts, setCharts] = useState([
  { id: 'chart1', component: <LineChart {...props1} />, loaded: false },
  { id: 'chart2', component: <BarChart {...props2} />, loaded: false },
]);

useEffect(() => {
  const loadCharts = async () => {
    for (let i = 0; i < charts.length; i++) {
      await new Promise(resolve => setTimeout(resolve, 100 * i));
      setCharts(prev => {
        const newCharts = [...prev];
        newCharts[i].loaded = true;
        return newCharts;
      });
    }
  };
  loadCharts();
}, []);

return (
  <div>
    {charts.map(chart => (
      <div key={chart.id}>
        {chart.loaded ? chart.component : <Loader />}
      </div>
    ))}
  </div>
);
  • 视口触发加载:用IntersectionObserver监听图表容器,进入视口时再加载:
// 封装LazyChart组件
const LazyChart = ({ children, threshold = 0.1 }) => {
  const ref = useRef<HTMLDivElement>(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => setIsVisible(entry.isIntersecting),
      { threshold }
    );
    ref.current && observer.observe(ref.current);
    return () => ref.current && observer.unobserve(ref.current);
  }, [threshold]);

  return <div ref={ref}>{isVisible ? children : <Loader />}</div>;
};

// 使用方式
<LazyChart>
  <LineChart {...props} />
</LazyChart>

5. 清理Highcharts实例,避免内存泄漏

在BaseChart中添加卸载清理逻辑,释放图表资源:

// BaseChart.tsx 中添加
useEffect(() => {
  return () => {
    if (chartRef.current?.chart) {
      chartRef.current.chart.destroy();
    }
  };
}, []);

6. 禁用非必要功能

全局禁用Highcharts的非必要功能,减少渲染开销:

// 在init-highcharts.ts中添加
Highcharts.setOptions({
  credits: { enabled: false }, // 禁用版权信息
  exporting: { enabled: false }, // 禁用导出功能(不需要则关闭)
});

内容的提问来源于stack exchange,提问作者Agathe Soubiran

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 11:07:05