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

React中D3十万级散点图缩放性能优化咨询

问题描述

我用D3在React中实现了包含约10万个数据项的散点图,支持鼠标滚轮缩放,但缩放交互速度极慢。希望找到优化方案,或了解是否需要更换实现方式/技术。

我的useEffect实现代码如下:

useEffect(() => {
        //setting up container width and height
        const w = width;
        const h = height;

        // create the result chart
        const resultChart = d3.select(resultChartRef.current)
            .attr('width', w)
            .attr('height', h)
            .style('margin-top', '10px');

        // Create the latent chart
        const latentChart = d3.select(latentChartRef.current)
            .attr('width', w)
            .attr('height', h)
            .style('margin-top', '10px');

            // d3.select(resultChartRef.current).selectAll().remove().exit()
        
        // Compute the scales of the two charts
        const resultScales = setScalesChart(dimRedData?.data?.result, w, h)
        const latentScales = setScalesChart(dimRedData?.data?.latent_space, w, h)

        if (currentZoomState) {
            const newXScale = currentZoomState.rescaleX(resultScales[0])
            const newYScale = currentZoomState.rescaleX(resultScales[1])
            resultScales[0].domain(newXScale.domain())
            resultScales[1].domain(newYScale.domain())
        };

        const xAxis = d3.axisBottom(resultScales[0]).ticks(10)
        const yAxis = d3.axisLeft(resultScales[0]).ticks(10)

        const gX = resultChart.append('g').call(xAxis).attr('transform', `translate(0, ${h})`);
        const gY = resultChart.append('g').call(yAxis);

        // TODO: styling properties http://www.d3noob.org/2014/02/styles-in-d3js.html
        resultChart.selectAll('*').remove();
        resultChart.selectAll('circle')
            // select dataset
            .data(filterData(dimRedData?.data?.result))
            .enter()
            // Assign data value
            .append('circle')
                .attr('cx', d => resultScales[0](d[0]))
                .attr('cy', d => resultScales[1](d[1]))
                .attr('r', 2)
            // Add item colour
            .style("fill", function(d, i) {
                return dimRedData.data.colour[0][i]
            })
            // Set border width
            .style("stroke-width", 0.5)
            // Add border colour
            .style("stroke", "black");
            
        latentChart.selectAll()
            // select dataset
            .data(filterData(dimRedData?.data?.latent_space))
            .enter()
            // Assign data value
            .append('circle')
                .attr('cx', d => latentScales[0](d[0]))
                .attr('cy', d => latentScales[1](d[1]))
                .attr('r', 2)
            // Add item colour
            .style("fill", function(d, i) {
                return dimRedData.data.colour[0][i]
            })
            // Set border width
            .style("stroke-width", 0.5)
            // Add border colour
            .style("stroke", "black");

        // Zoom of the chart
        const zoomBehavior = d3.zoom()
            .extent([[0, 0],[w, h]])
            .scaleExtent([0.5, 5])
            .translateExtent([[0,0], [w, h]])
            .on("zoom", (e) => {
                const zoomState = d3.zoomTransform(resultChart.node())
                setCurrentZoomState(zoomState);
                console.log("ZOOM!")
            });
        
        // Add zoom functionality to the result chart
        resultChart.call(zoomBehavior)
        
    }, [random, projectionFullScreen, currentZoomState]);
优化方案

1. 跳过React状态触发的全量重绘

当前缩放时通过setCurrentZoomState更新React状态,触发useEffect重新执行,进而删除所有元素并重建10万个circle,这是卡顿核心原因。直接在D3的zoom事件中操作DOM,跳过React状态更新:

  • 将所有circle放入一个<g>容器,缩放时仅更新该容器的transform属性,无需重新计算每个circle的位置
  • 轴的更新也直接在zoom事件中完成,不用每次重建轴元素

修改示例:

// 仅在数据或尺寸变化时初始化,避免重复执行
useEffect(() => {
  const w = width;
  const h = height;
  const resultChart = d3.select(resultChartRef.current)
    .attr('width', w)
    .attr('height', h)
    .style('margin-top', '10px');

  // 初始化点容器、轴容器(仅创建一次)
  const pointsG = resultChart.append('g').attr('class', 'points');
  const xAxisG = resultChart.append('g').attr('class', 'x-axis').attr('transform', `translate(0, ${h})`);
  const yAxisG = resultChart.append('g').attr('class', 'y-axis');

  // 初始化比例尺和轴
  let resultScales = setScalesChart(dimRedData?.data?.result, w, h);
  let xAxis = d3.axisBottom(resultScales[0]).ticks(10);
  let yAxis = d3.axisLeft(resultScales[1]).ticks(10);
  xAxisG.call(xAxis);
  yAxisG.call(yAxis);

  // 绘制初始点(仅执行一次)
  pointsG.selectAll('circle')
    .data(filterData(dimRedData?.data?.result))
    .enter()
    .append('circle')
    .attr('cx', d => resultScales[0](d[0]))
    .attr('cy', d => resultScales[1](d[1]))
    .attr('r', 2)
    .style("fill", (d, i) => dimRedData.data.colour[0][i])
    .attr('class', 'scatter-point');

  // 缩放行为定义
  const zoomBehavior = d3.zoom()
    .extent([[0, 0],[w, h]])
    .scaleExtent([0.5, 5])
    .translateExtent([[0,0], [w, h]])
    .on("zoom", (e) => {
      // 更新点容器的变换
      pointsG.attr('transform', e.transform);
      // 更新轴
      const newXScale = e.transform.rescaleX(resultScales[0]);
      const newYScale = e.transform.rescaleY(resultScales[1]);
      xAxisG.call(d3.axisBottom(newXScale).ticks(10));
      yAxisG.call(d3.axisLeft(newYScale).ticks(10));
    });

  resultChart.call(zoomBehavior);

}, [dimRedData, width, height]);

2. 替换SVG为Canvas/WebGL

10万个SVG元素会占用大量内存和渲染资源,换成Canvas或WebGL能大幅提升性能:

  • 使用D3结合Canvas API绘制点,缩放时直接重绘Canvas内容,比更新10万个SVG元素快得多
  • 追求极致性能可使用WebGL渲染(比如自定义着色器或结合WebGL可视化库)

3. 优化样式与数据处理

  • 将公共样式(stroke-width、stroke)提取为CSS类,避免每个元素单独设置:
.scatter-point {
  stroke: black;
  stroke-width: 0.5;
}
  • 提前预计算颜色数组,避免在绘制时动态索引dimRedData.data.colour[0][i],可在组件外部或useMemo中生成映射数组

4. 减少不必要的DOM操作

  • 移除resultChart.selectAll('*').remove();,改用D3的enter/update/exit模式管理元素,只更新变化部分,不全量删除重建
  • 避免在useEffect中重复创建相同的D3选择器和轴实例
技术替换建议

如果上述优化后仍无法满足性能要求,可以考虑:

  • 使用专注于大规模数据可视化的库,比如Plotly.js(支持WebGL渲染散点图)、Deck.gl(基于WebGL的高性能可视化)
  • 直接使用Canvas/WebGL原生API实现,完全跳过SVG的性能瓶颈

内容的提问来源于stack exchange,提问作者Martijn Noordhof

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 22:25:24