首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Vue3 打造 AIOps Dashboard:实时监控面板的开发实战,性能优化全指南

Vue3 打造 AIOps Dashboard:实时监控面板的开发实战,性能优化全指南

作者头像
行者全栈架构师
发布2026-07-22 12:40:31
发布2026-07-22 12:40:31
100
举报

某次大促期间,运维 Dashboard 突然卡死,页面白屏长达 30 秒。排查发现是 ECharts 同时渲染 8 个大图表导致内存溢出。那次事故让我意识到,运维前端不是简单堆组件,需要从架构层面做性能优化和实时数据流管控。

01 Dashboard 架构设计

我负责的 AIOps Dashboard 需要同时展示 10+ 个服务的实时指标、告警列表和任务状态,架构设计是确保性能的基础。

组件结构一览

组件

类型

职责

数据来源

MetricCard

展示组件

单指标卡片(CPU/内存/QPS)

metricStore

AlertTimeline

展示组件

告警时间线

alertStore

RealtimeChart

展示组件

ECharts 实时折线图

useMetric

TaskBoard

展示组件

任务看板(待执行/执行中/已完成)

taskStore

ServiceMap

展示组件

服务拓扑图

metricStore

DashboardLayout

布局组件

响应式栅格布局

-

图:Vue3 AIOps Dashboard 完整效果图,展示服务拓扑、实时指标卡片、ECharts 趋势图、告警列表和任务看板

02 WebSocket 数据推送

前端数据流转时序

为什么需要画这张时序图?WebSocket 数据从前端连接到最终渲染,涉及多层流转:浏览器→Vue App→WebSocket Client→后端→Pinia Store→ECharts。理清这条链路,才能准确定位性能瓶颈和断线重连的切入点。

useWebSocket 组合函数

为什么把 WebSocket 逻辑抽成 Composable?多个页面组件都需要 WebSocket 连接,抽成 Composable 可以复用连接逻辑、统一管理断线重连,避免每个组件重复实现。

代码语言:javascript
复制
/**
 * WebSocket 组合函数
 * 封装连接、心跳、断线重连逻辑
 */
import {ref, onUnmounted} from 'vue'
import {useWsStore} from '@/stores/wsStore'

interface WsOptions {
  url: string
  heartbeatInterval?: number  // 心跳间隔,默认 30s
  reconnectDelay?: number     // 重连延迟,默认 5s
  maxRetries?: number         // 重试次数,默认 10
}

export function useWebSocket(options: WsOptions) {
  const {
    url,
    heartbeatInterval = 30000,
    reconnectDelay = 5000,
    maxRetries = 10,
  } = options

  const wsStore = useWsStore()
  const connected = ref(false)
  const retryCount = ref(0)
  let ws: WebSocket | null = null
  let heartbeatTimer: ReturnType<typeof setInterval> | null = null

  function connect() {
    ws = new WebSocket(url)

    ws.onopen = () => {
      connected.value = true
      retryCount.value = 0
      wsStore.setStatus('connected')
      startHeartbeat()
    }

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      if (data.type === 'alert') {
        wsStore.dispatchAlert(data.payload)
      } else if (data.type === 'metric') {
        wsStore.dispatchMetric(data.payload)
      } else if (data.type === 'heartbeat') {
        return
      }
    }

    ws.onclose = () => {
      connected.value = false
      wsStore.setStatus('disconnected')
      stopHeartbeat()
      attemptReconnect()
    }

    ws.onerror = () => {
      ws?.close()
    }
  }

  function startHeartbeat() {
    heartbeatTimer = setInterval(() => {
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({type: 'ping'}))
      }
    }, heartbeatInterval)
  }

  function stopHeartbeat() {
    if (heartbeatTimer) {
      clearInterval(heartbeatTimer)
      heartbeatTimer = null
    }
  }

  function attemptReconnect() {
    if (retryCount.value >= maxRetries) {
      wsStore.setStatus('failed')
      return
    }
    retryCount.value++
    setTimeout(connect, reconnectDelay)
  }

  function disconnect() {
    stopHeartbeat()
    ws?.close()
    ws = null
  }

  onUnmounted(() => {
    disconnect()
  })

  return {connected, retryCount, connect, disconnect}
}

03 ECharts 实时图表

useMetric 组合函数

为什么用 Composable 管理 ECharts 而不是直接在组件里写?指标计算逻辑(滑动窗口、聚合、格式化)与图表渲染解耦后,可以在不同图表类型间复用计算逻辑。

代码语言:javascript
复制
import {ref, computed, watch} from 'vue'
import type {EChartsOption} from 'echarts'
import {useMetricStore} from '@/stores/metricStore'

interface MetricConfig {
  serviceName: string
  metricName: string
  windowSize?: number
}

export function useMetric(config: MetricConfig) {
  const {serviceName, metricName, windowSize = 60} = config
  const metricStore = useMetricStore()
  const dataPoints = ref<{ time: number; value: number }[]>([])

  watch(
    () => metricStore.getLatest(serviceName, metricName),
    (newPoint) => {
      if (!newPoint) return
      dataPoints.value.push(newPoint)
      if (dataPoints.value.length > windowSize) {
        dataPoints.value.shift()
      }
    }
  )

  const chartOption = computed<EChartsOption>(() => ({
    tooltip: {trigger: 'axis'},
    grid: {left: 50, right: 20, top: 20, bottom: 30},
    xAxis: {
      type: 'category',
      data: dataPoints.value.map((p) =>
        new Date(p.time).toLocaleTimeString()
      ),
    },
    yAxis: {type: 'value', scale: true},
    series: [{
      type: 'line',
      data: dataPoints.value.map((p) => p.value),
      smooth: true,
      showSymbol: false,
      lineStyle: {width: 2},
      areaStyle: {opacity: 0.1},
    }],
    animation: false,
  }))

  return {dataPoints, chartOption}
}

RealtimeChart 组件

为什么关闭 ECharts 动画?实时数据每秒更新一次,开启动画会导致图表闪烁和 CPU 飙升,关闭后渲染更流畅。

代码语言:javascript
复制

<template>
  <div class="realtime-chart">
    <div class="chart-header">
      <span class="chart-title">{{ title }}</span>
      <el-tag :type="statusType" size="small">
        {{ currentValue }}
      </el-tag>
    </div>
    <div ref="chartRef" class="chart-container"/>
  </div>
</template>

<script setup lang="ts">
  import {ref, onMounted, onUnmounted, watch} from 'vue'
  import * as echarts from 'echarts'
  import {useMetric} from '@/composables/useMetric'

  const props = defineProps<{
    title: string
    serviceName: string
    metricName: string
    unit?: string
  }>()

  const chartRef = ref<HTMLDivElement>()
  let chartInstance: echarts.ECharts | null = null

  const {chartOption, dataPoints} = useMetric({
    serviceName: props.serviceName,
    metricName: props.metricName,
  })

  const currentValue = computed(() => {
    const last = dataPoints.value[dataPoints.value.length - 1]
    return last ? `${last.value}${props.unit || ''}` : '-'
  })

  const statusType = computed(() => {
    const last = dataPoints.value[dataPoints.value.length - 1]
    if (!last) return 'info'
    if (last.value > 80) return 'danger'
    if (last.value > 60) return 'warning'
    return 'success'
  })

  onMounted(() => {
    if (chartRef.value) {
      chartInstance = echarts.init(chartRef.value)
      chartInstance.setOption(chartOption.value)
    }
  })

  watch(chartOption, (newOption) => {
    chartInstance?.setOption(newOption)
  })

  onUnmounted(() => {
    chartInstance?.dispose()
  })
</script>

<style scoped>
  .chart-container {
    width: 100%;
    height: 200px;
  }

  .chart-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8px;
  }
</style>

04 Pinia 状态管理

告警 Store 设计

为什么告警 Store 要维护已读/未读状态?AIOps Dashboard 需要在标题栏显示未读告警数量,同时告警列表需要标记已读,这需要集中管理状态。

代码语言:javascript
复制
import {defineStore} from 'pinia'
import {ref, computed} from 'vue'
import type {Alert} from '@/types/alert'

export const useAlertStore = defineStore('alert', () => {
  const alerts = ref<Alert[]>([])
  const readIds = ref<Set<string>>(new Set())
  const severityFilter = ref<string>('all')

  const unreadCount = computed(() => {
    return alerts.value.filter(
      (a) => !readIds.value.has(a.id)
    ).length
  })

  const filteredAlerts = computed(() => {
    if (severityFilter.value === 'all') return alerts.value
    return alerts.value.filter(
      (a) => a.severity === severityFilter.value
    )
  })

  function addAlert(alert: Alert) {
    alerts.value.unshift(alert)
    if (alerts.value.length > 200) {
      alerts.value.pop()
    }
  }

  function markAsRead(id: string) {
    readIds.value.add(id)
  }

  function markAllAsRead() {
    alerts.value.forEach((a) => readIds.value.add(a.id))
  }

  return {
    alerts, readIds, severityFilter,
    unreadCount, filteredAlerts,
    addAlert, markAsRead, markAllAsRead,
  }
})

05 响应式布局

Dashboard 栅格配置

为什么用 CSS Grid 而不是 Element Plus 的 el-row/el-col?AIOps Dashboard 有复杂的跨列跨行布局,CSS Grid 的 grid-template-areas 更直观,维护成本更低。

代码语言:javascript
复制

<template>
  <div class="dashboard-grid">
    <div class="area-cpu">
      <MetricCard title="CPU 使用率" :value="cpuValue" unit="%"/>
    </div>
    <div class="area-memory">
      <MetricCard title="内存使用率" :value="memValue" unit="%"/>
    </div>
    <div class="area-qps">
      <MetricCard title="请求量 QPS" :value="qpsValue"/>
    </div>
    <div class="area-chart">
      <RealtimeChart
        title="服务指标趋势"
        service-name="order-service"
        metric-name="cpu_usage"
        unit="%"
      />
    </div>
    <div class="area-alerts">
      <AlertTimeline/>
    </div>
    <div class="area-tasks">
      <TaskBoard/>
    </div>
  </div>
</template>

<style scoped>
  .dashboard-grid {
    display: grid;
    grid-template-areas:
    "cpu    memory  qps"
    "chart  chart   alerts"
    "tasks  tasks   alerts";
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: auto 1fr 1fr;
    gap: 16px;
    padding: 16px;
    min-height: calc(100vh - 60px);
  }

  @media (max-width: 1024px) {
    .dashboard-grid {
      grid-template-areas:
      "cpu    memory"
      "qps    qps"
      "chart  chart"
      "alerts tasks";
      grid-template-columns: 1fr 1fr;
    }
  }

  @media (max-width: 640px) {
    .dashboard-grid {
      grid-template-areas:
      "cpu"
      "memory"
      "qps"
      "chart"
      "alerts"
      "tasks";
      grid-template-columns: 1fr;
    }
  }

  .area-cpu {
    grid-area: cpu;
  }

  .area-memory {
    grid-area: memory;
  }

  .area-qps {
    grid-area: qps;
  }

  .area-chart {
    grid-area: chart;
  }

  .area-alerts {
    grid-area: alerts;
  }

  .area-tasks {
    grid-area: tasks;
  }
</style>

图:AIOps Dashboard 移动端自适应效果,竖屏模式下卡片、图表、告警列表纵向排列

06 踩坑指南

坑 1:ECharts 实例泄漏导致内存飙升

现象:Dashboard 打开 1 小时后,Chrome 内存从 200MB 飚到 1.5GB,页面卡顿。

原因:切换路由时 ECharts 实例未销毁,每次进入页面创建新实例,旧实例的定时器和 DOM 引用持续占用内存。

解决:在 onUnmounted 中确保销毁实例,并用 WeakMap 做实例缓存:

代码语言:javascript
复制
const chartInstances = new WeakMap<HTMLDivElement, echarts.ECharts>()

function getOrCreateChart(el: HTMLDivElement) {
  let chart = chartInstances.get(el)
  if (!chart) {
    chart = echarts.init(el)
    chartInstances.set(el, chart)
  }
  return chart
}

onUnmounted(() => {
  const chart = chartInstances.get(chartRef.value!)
  chart?.dispose()
  chartInstances.delete(chartRef.value!)
})

提醒:Vue 的 <keep-alive> 也会保留组件实例,配合 onDeactivated 暂停图表更新。

坑 2:WebSocket 重连后告警列表重复

现象:WebSocket 断线重连后,Dashboard 出现重复告警,同一条告警出现 2-3 次。

原因:重连后后端会补发断线期间的告警,但前端 Store 没有做去重处理,新数据和已有数据叠加。

解决:在 addAlert 时增加去重校验:

代码语言:javascript
复制
function addAlert(alert: Alert) {
  const exists = alerts.value.some((a) => a.id === alert.id)
  if (exists) return
  alerts.value.unshift(alert)
  if (alerts.value.length > 200) {
    alerts.value.pop()
  }
}

提醒:批量补发场景下,建议后端接口支持 sinceTimestamp 参数,只返回断线后的增量数据。

坑 3:ECharts resize 后图表空白

现象:侧边栏折叠/展开后,图表区域变为空白,需要手动刷新页面才恢复。

原因:ECharts 在容器尺寸变化时不会自动 resize,渲染还是按旧尺寸。

解决:监听 ResizeObserver 触发 ECharts resize:

代码语言:javascript
复制
let resizeObserver: ResizeObserver | null = null

onMounted(() => {
  const chart = getOrCreateChart(chartRef.value!)
  resizeObserver = new ResizeObserver(() => {
    chart.resize()
  })
  if (chartRef.value) {
    resizeObserver.observe(chartRef.value)
  }
})

onUnmounted(() => {
  resizeObserver?.disconnect()
})

提醒:多个图表同时 resize 会造成性能问题,建议用 requestAnimationFrame 做节流。

07 渲染性能对比

优化项

优化前

优化后

提升幅度

ECharts 动画

开启(60fps 占用 15% CPU)

关闭(CPU 占用 3%)

降低 80%

告警列表渲染

200 条全量渲染

虚拟滚动(可视区 20 条)

内存降低 60%

WebSocket 心跳

无(SLB 频繁断开)

30s 心跳

连接稳定率 99%

图表 resize

手动刷新

ResizeObserver 自动

零操作修复

测试环境:Chrome 126 | MacBook Pro M2 | Dashboard 同时展示 6 个图表 + 告警列表 + 任务看板

图:Vue3 AIOps Dashboard 性能优化效果对比,优化后各项指标显著提升

08 总结

本文覆盖了 AIOps Dashboard 前端开发的四个核心环节:

  • ✅ Vue3 Composition API 架构设计
  • ✅ ECharts 实时图表与 WebSocket 数据推送
  • ✅ Pinia 状态管理(告警、指标、任务)
  • ✅ 响应式布局与性能优化

适用场景:需要搭建运维监控 Dashboard 的前端开发者

不适用场景:已有 Grafana 等成熟监控面板且无需定制的团队

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 行者架构谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01 Dashboard 架构设计
    • 组件结构一览
  • 02 WebSocket 数据推送
    • 前端数据流转时序
    • useWebSocket 组合函数
  • 03 ECharts 实时图表
    • useMetric 组合函数
    • RealtimeChart 组件
  • 04 Pinia 状态管理
    • 告警 Store 设计
  • 05 响应式布局
    • Dashboard 栅格配置
  • 06 踩坑指南
    • 坑 1:ECharts 实例泄漏导致内存飙升
    • 坑 2:WebSocket 重连后告警列表重复
    • 坑 3:ECharts resize 后图表空白
  • 07 渲染性能对比
  • 08 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档