项目:html5-dataviz | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本项目的性能画像非常极端:代码资产 1688 个案例、约 1GB 静态资产、三个重量级图表/三维运行时,但首屏只需一个导航首页。因此全部性能设计的核心策略一句话可以概括:懒加载一切重资产,让"不用的代码一个字节都不下载"。三个支柱在 01 篇第 1 节已有定性描述——JSON 动态 import(曾把约 20MB 打进 base chunk,注释自证)、import.meta.glob 非 eager 切分 1617 个 SFC、echarts/three/g2plot 全部动态引入;本章逐层量化并复盘关键事故。先看当前构建产物(dist/,pnpm build 实测)的主要 chunk 体积,它是后续所有讨论的数据基础:
| Chunk(dist/assets/) | 体积 | 加载时机 |
|---|---|---|
| three-vendor-*.js | 5.4MB | three 示例详情页 |
| material-symbols-outlined-*.woff2 | 3.96MB | 首屏(图标字体,src/main.ts 引入) |
| webgl_loader_ifc-*.js | 3.5MB | 仅 IFC 模型示例 |
| spark.module-*.js | 2.5MB | 仅 @sparkjsdev/spark 相关示例 |
| ChartExampleDetailPage-*.js | 2.3MB | 任一图表详情页 |
| vendor-*.js(框架运行时桶) | 2.1MB | 首屏(modulepreload) |
| echarts-*.js(echarts 核心,EChartDemo 静态 import) | 1.19MB | echarts 示例详情页 |
| echarts-vendor-*.js | 1.18MB | echarts 示例详情页 |
| antv-vendor-*.js | 846KB | antv 兜底详情页 |
| echarts-gl-vendor-*.js | 600KB | GL 类兜底详情页 |
| echarts.json / three.json / antv.json | 1.22MB / 821KB / 245KB | 首次访问对应库的列表/详情 |
首屏关键路径实测只有四件东西:index-*.js 入口 + vendor-*.js 框架桶 + 两个 CSS(含字体声明的 vendor CSS 与 index CSS)+ material-symbols 字体。dist/index.html 中 modulepreload 只有 vendor 一条,echarts/three/antv 的任何 chunk 都不在首屏——这正是 9.3MB 事故修复后的理想形态。
测量方法论:本篇所有数字的复现方式有三条路。其一是构建产物静态分析——pnpm build 后对 dist/assets/ 按体积排序(本文表格即如此产生),配合查看 dist/index.html 的 modulepreload 列表判断首屏依赖边;其二是浏览器侧动态验证——Network 面板按域过滤后刷新首页,确认没有任何图表库 chunk 被下载(00 篇第 6 节把它列为三条健康启动信号之一),Performance 面板录制详情页加载可看到"JSON chunk → vendor chunk → 示例 SFC chunk"的瀑布次序;其三是 vite build --report 类插件或 rollup-plugin-visualizer(当前未安装,属可选增强)。三条路的交叉验证很重要:静态体积不等于传输体积(gzip/br 后 JSON 类文本收益最大),也不等于运行时体积(解析与编译成本与 chunk 大小非线性相关)。需要特别提醒的是,本项目的体积分布高度偏斜——前五个 chunk 就占了约 17MB,优化注意力应集中在头部而非平均数。
vite.config.ts 的 build.rollupOptions.output.manualChunks 是一个函数式分包器,规则按序短路:非 node_modules 一律返回 undefined(注释:"仅归组三方依赖;src 代码(含按库懒加载的图表 JSON)不进 vendor 桶");随后五桶依次匹配——echarts-gl → echarts-gl-vendor;/echarts、vue-echarts、@echarts-x → echarts-vendor;@antv → antv-vendor;/three → three-vendor;最后框架运行时桶 vendor 收编 /@vue/、/vue/、vue-router、/pinia/、@vueuse、element-plus、/dayjs/。注意匹配顺序的隐含语义:echarts-gl 桶必须先于 echarts 桶(否则 echarts-gl 会因路径含 echarts 落错桶);/echarts 带斜杠前缀是为了避免误吞路径中恰好含 echarts 字样的其他包。
vendor 桶规则中留有一段直接注释了事故的代码:"框架运行时独立成 chunk:此前 Vue 运行时被并进 echarts-vendor,导致入口 chunk 静态依赖并 modulepreload 了 9.3MB 的 echarts"。还原事故链条需要三个事实:其一,Vue 运行时(/vue/ 路径的包)与 echarts 同属 node_modules,在无 manualChunks 或规则不当时会被 rollup 合并进同一个共享 chunk;其二,入口链路上存在对 echarts 的静态依赖边——src/utils/v-chart.ts 的历史版本或 EChartDemo 被入口静态可达时,echarts-vendor 成为入口 chunk 的依赖;其三,Vite 会为入口的所有直接依赖注入 modulepreload,于是 9.3MB(彼时 echarts 全量 + Vue 运行时合并体积)被写进 index.html 的 preload 列表,首屏用户无论访问哪个页面都要下载。修复是双向的:src/utils/v-chart.ts 改为 defineAsyncComponent 包两级动态 import(头注释自述这是对 2026-10-03 大屏 v-chart 缺注册事故的同一波修复),切断入口静态依赖边;manualChunks 把框架运行时单独收进 vendor 桶,保证入口的 modulepreload 永远只指向轻量桶。验证方式今天仍可复现:看 dist/index.html 的 preload 列表(只有 vendor),或在浏览器 Network 面板确认首页不加载任何图表库代码(00 篇第 6 节把它列为健康的启动信号之一)。
事故修复后仍有一个值得注意的静态依赖:src/chart-examples/EChartDemo.vue 顶部 import * as echarts from 'echarts'(注释:"全量 echarts 已在模块顶部静态引入(painter 注册时序确定)"),而所有 294 个 echarts SFC 静态 import EChartDemo。结果是访问任一 echarts 示例详情页都要下载 echarts 核心 chunk(1.19MB)+ echarts-vendor(1.18MB)+ ChartExampleDetailPage chunk(2.3MB,含 Renderer 链)约 4.7MB。这是"注册时序确定性"换来的体积代价,属有意取舍;若要优化见第 8 节建议 1。
src/views/ChartExampleDetailPage.vue 顶层建了两个 glob 表:localModules = import.meta.glob('@/chart-examples/**/*.vue')(组件加载器)与 localSources = import.meta.glob('@/chart-examples/**/*.vue', { query: '?raw', import: 'default' })(源码字符串加载器)。非 eager 模式下,Vite 为每个匹配文件生成独立异步 chunk,glob 表本身只是一个"路径 → () => import()"的映射对象,构建期零执行成本、运行时按需触发。1617 个 SFC 因此各成独立分片,详情页只拉当前 id 的那一个。?raw 双表是源码弹窗与下载功能的基础——源码不占组件 chunk 体积,只在打开弹窗/下载时才拉。
数据侧,src/api/chart-examples.ts 的 DATASET_LOADERS 每库一个动态 import JSON 加载器,datasetCache 缓存 Promise 实现库级单次加载(失败清缓存允许重试)。三个 JSON slim 后的体积(echarts 1.22MB / three 821KB / antv 245KB)意味着首次进 echarts 列表要下载 1.22MB 元数据——这是纯静态站的固有成本(列表页一次拉全量再内存分页),JSON 已经过 scripts/slim-chart-json.mjs 瘦身(有 SFC 的条目删代码字段),若要再压可考虑 gzip/br 传输压缩(1.2MB JSON 文本压缩比通常 5 倍以上,属服务器配置而非代码改动)。
浏览器对同一页面同时存活的 WebGL 上下文有 816 个的上限(各内核不一),超限后新建 context 直接失败,表现为黑画布且不可恢复。本项目两类场景会反复创建 context:详情页"刷新预览"= 重挂载组件(renderKey++),以及快速切换示例。防护在 16 个 context 数上限)"。dispose 序列完整:trackedListeners 逐个 removeEventListener → trackedRafIds 逐个 cancelAnimationFrame → resizeObserver.disconnect → renderLoop.stop → controls.dispose → src/chart-examples/three/stage/useExampleStage.ts 的 dispose 中:文件头注释明说与上游 curio 的差异只有两处,其一就是"dispose 里追加 forceContextLoss(本项目详情页'刷新=重挂载'且无 curio ExampleHost 的 loseContext 安全网,需在此主动丢弃 WebGL 上下文,防止反复重挂载撞上浏览器 8disposeObject3D(scene)(递归释放几何体/材质/纹理)→ renderer.dispose() → renderer.forceContextLoss()。echarts 侧的对应物是 Renderer 的 cleanupChart 调 chart.destroy() 与 EChartDemo 的 cleanupFn 机制——三者合起来构成"重挂载不泄漏"的完整防线,任一环节被绕过(如新写示例时自行创建 renderer 而不走 ExampleScene)都会重现 context 撞限。
大屏的渲染负载特征是"一屏十几个图表 + 持续动画 + 长时不关"。项目的应对分布在四层:
src/components/datav/DatavContainer.vue 以 1920×1080 设计稿为基准,按视口宽高分别计算 scale 后 transform 缩放(比逐元素响应式布局便宜一个数量级),resize 监听经防抖,另用 MutationObserver 兜住容器内部结构变化(文件头注释说明设计);sc-datav-demo0/1 两屏另走 autofit.js 方案(各自 components/AutoFit.vue 封装 autofit.init),是迁移存量保留下来的第二种缩放实现(⚠️ 待确认是否长期并存还是逐步统一到 DatavContainer);<v-chart> 消费全局懒加载注册,manual-update 避免vue-echarts 对 option 做深度响应式 diff(大屏 option 巨大且多为整体替换,diff 纯浪费);autoresize 保证容器缩放后图表重排;src/composables/useScreenData.ts 的 3 秒定时自增(单一时钟源,避免每图表各自 setInterval),report 大屏的静态 JSON 经 Pinia 一次加载(loading 中重复调用直接返回);bigscreenComponentMap 逐屏独立 chunk,列表页不加载任何大屏实现;Theatre.js(Theatrejs chunk 875KB)等重型依赖只被用到它的大屏按需拉取。大屏性能问题有一个与图表示例完全不同的排查视角:示例页面的负载是"访问即峰值、离开即清零",而大屏是长时驻留场景——监控中心的大屏可能连续运行数天不刷新,任何微小的泄漏(未清理的 setInterval、未移除的事件监听、逐帧累积的数组)都会以天为单位放大成内存耗尽。因此大屏侧的性能纪律比示例页严格得多:定时器必须走 onMounted/onBeforeUnmount 成对出现(SalesAnalysisPage.vue 的时间戳刷新是标准示范)、模拟数据流统一收敛到 useScreenData 的单一时钟源而非每图表自建 interval、DatavCountTo 等动画组件的补间要在组件卸载时终止。此外,22 个大屏中有 4 个是 3D 场景(sc-datav-demo0~3),它们同时受 WebGL context 上限防护(第 4 节)与大屏缩放层双重影响——DatavContainer 的 transform 缩放不会改变 canvas 的实际像素尺寸,3D 大屏的渲染分辨率由 WebGLRenderer 自身的 setPixelRatio/size 决定,两者叠加时要确认缩放后 canvas 尺寸计算没有二次失真。
SFC 沙箱编译是运行时最贵的单点操作(parse + compileScript + compileTemplate + compileStyle + 依赖并行加载)。三层成本控制:sfcRuntime.compilerCache 以 demoId 为键缓存编译产物(同一示例二次渲染零编译);VueSfcRenderer 的 lazy 模式用 IntersectionObserver(rootMargin 600px)+ mouseenter 双触发惰性编译,卡片列表场景不在视口的卡片不编译;compileWhenReady 以 60 帧为上限等待容器获得非零高度再编译(MAX_LAYOUT_WAIT_FRAMES),避免在父级未完成布局时编译后立即测量失败。列表页 24 条一页的分页(usePagination 默认 pageSize 24)本身也是性能设计——分页是前端内存操作,但决定了 DOM 节点上限。另外 useListFilter 的 300ms 防抖保证 1688 条内存过滤不会每个按键触发一次。
public/ 约 1GB 资产按"引用才拷贝"原则治理过一轮(scripts/gen-three-registry.mjs 第 3 步"资产裁剪拷贝:扫描拷入源码里的静态路径字面量,只拷贝真实命中的文件"),三大 three 目录(threejs-official 429M / three-project 195M / external-examples 188M)是按引用裁剪后的结果。缩略图经 scripts/gen-thumbs-webp.mjs 批量转 WebP(宽 640、质量 78),列表页网格一次加载 24 张,WebP 相对 PNG 的体积收益约 60~70%。加载失败容错:缩略图 onerror 进 failedThumbnails Set 回落 📊 占位符,不影响主流程。
资产侧还有一个容易被忽视的隐性成本:同一数据的双轨存放。GeoJSON 双份(public/map-geojson/ 供运行时 fetch、src/assets/geo/ 供构建期内联进大屏 chunk)、地图注册数据 china.json(3.5MB)与 world.json(1.5MB)未做拓扑简化(简化后的低精度版通常可缩到十分之一),省级 34 个 GeoJSON 合计约 4MB 常驻 public。对内网千兆环境这些都不构成瓶颈,这正是本项目与公网站点的性能观差异——这里的性能预算优先花在"首屏轻、按需快",而不是"资产瘦";但若部署到低带宽环境(如 4G 路由的移动大屏车),资产瘦身应提升优先级,可用 mapshaper 类工具离线简化后原路径替换,不动任何代码。
按预期收益排序,均标注代价:
ms:xxx)。可按 CATEGORY_ICONS 等映射表收集实际图标名做子集化(google/fonts 工具链),预计可减 90% 以上;代价是新增图标需重新生成字体;