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

