项目:html5-examples | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
理解本项目的性能设计,先记住三个数字:750 个示例约 8.3MB 原文 JSON(历史迁移基线 1057 个/约 8.6MB,2026-10-06 内容治理后)、1182 个动态导入组件(chunk 5000+,源码注释口径)、9 个重型图形库 + Monaco + ECharts + Shiki 等大体积依赖。三者分别威胁"数据下载""构建时间与 chunk 治理""首屏依赖"三个环节。项目的性能方案可以概括为一条主线:把"下载什么、执行什么"的决定权从构建期尽量推迟到运行时的最后一步——数据按分组懒加载、组件按路由懒加载、重库按使用场景拆 chunk、预览 iframe 按可视区渲染。以下分节展开每个环节的实现、代价与剩余优化空间。
实现(src/api/examples.ts,机制走读见《07-模块详解》§1.2):8 个分组 JSON 各生成一个独立 chunk,loadAll() 用 Promise.all 并发拉取;cache(结果)与 loadingPromise(进行中的 Promise)双变量实现"加载一次、终身缓存";合并后统一稳定排序,之后的全部筛选/分页在内存完成,零网络请求。
代价分析:首次进入 /examples 不带筛选时会下载全部 8 个分组(这是唯一的全量场景——分组即文件,接口签名里没有"只拉一个分组"的入口);约 8.3MB 原文经 gzip 后体积显著缩小(JSON 文本压缩比通常 5-10 倍,实测应以部署环境为准,⚠️ 待确认),但解析与合并 750 个对象仍是主线程一次性成本。缓解因素有三:一是分组 chunk 带 content hash,二次访问走 HTTP 缓存零下载;二是模块级缓存让单页应用整个生命周期只付一次;三是排序/过滤后分页切片,渲染层还有虚拟滚动(VirtualExampleGrid)兜底 DOM 数量。
已识别的下一步优化(《05-核心流程》流程一的解读中已指出,此处正式立项):让 loadAll() 支持按需分组加载——getExamples({ sourceGroup }) 时只 import 对应分组,getExampleById 等全量场景再走全量。改造点集中在 groupLoaders 表与 cache 结构(从数组改为按分组的 Map + 合并视图),接口签名不变。收益:按主题浏览的主路径下载量降为 1/8 左右;代价:ids/search 等跨分组查询需要全量兜底,逻辑分叉增加。
iframe srcdoc 是本项目隔离性的来源,也是最大的运行时成本:每个预览都是完整文档实例(独立 DOM 树、独立脚本上下文、独立样式计算)。项目用三个机制把成本压到"只有正在看的那个示例"才支付:
ExampleCard.vue/ExampleListItem.vue)展示的是 ExampleCodeSnapshot.vue——把示例源码前 12 行经 CodeBlock(Shiki)渲染、以 mask-image: linear-gradient(...) 渐隐截断的"代码快照"。这个设计一石二鸟:既避免了千卡千 iframe 的内存灾难,又把卡片视觉从"缩略截图"升级为"代码即内容"(快照组件实测存在且被两个卡片组件引用,此前文档中的"快照方向探索 ⚠️ 待确认"就此落定:它不是图片快照,而是代码快照)。PreviewPane.vue 用 IntersectionObserver 驱动 isVisible,未进入视口只渲染占位(preview-placeholder),进入才创建 DeviceFrame/iframe——详情页首屏也只付一个 iframe 的钱。刷新成本的优化细节:usePreview 的 srcDoc 是 computed,任何编辑输入都会重算 buildSrcDoc 并触发 iframe 重载——这是"实时预览"的本质成本,无法免除,但拼装本身是字符串操作(微秒级),真正的开销在 iframe 文档重建。编辑态的高频输入没有做防抖(liveExample 直通),属于有意取舍:牺牲少量主线程换取"敲一行立刻见"的确定性;若未来出现卡顿反馈,第一优化点是给 liveExample 同步加一层 150ms 防抖而非改沙箱。
manualChunks 按"每个示例只引入自己的库"原则布置了 19 组规则,关键分组与动机:Vue 全家桶(vue/vue-router/pinia/@vue)→ vendor(首屏唯一必载的库 chunk);@vue/compiler-sfc + esbuild → sfc-compiler(仅浏览器 SFC 运行时动态加载);echarts 家族按 echarts-gl/echarts-stat/echarts 三个粒度拆分;@antv → antv-vendor;three/gsap/cannon → 3D 与物理;pixi/fabric/konva/leafer → Canvas 各库独立;roughjs 独立(体积大)、signature_pad+p5 合并 → canvas-misc-vendor;@guolao/vue-monaco-editor 与 codemirror → editor-vendor;monaco-editor → monaco-vendor。chunkFileNames 把 vendor 类 chunk 命名为 assets/vendor-*-hash.js,与业务 chunk 区分,利于 CDN 缓存策略分组。
三条"事故换来的配置"必须原样保留(改动前先读注释):hoistTransitiveImports: false 防止 Rollup 把跨 chunk 传递依赖提升造成循环 TDZ;resolve.dedupe: ['gsap'] + optimizeDeps.include 全量列出 gsap 插件入口,强制单一 gsap core 实例(MotionPathPlugin 跨实例污染曾导致 "Indexed property setter" 报错);不启用 experimentalMinChunkSize——本地化后 1182 个动态组件使该合并算法接近 O(n²),打包曾耗时 1 小时以上,注释明示"勿回退"。
Monaco:SourcePane.vue → MonacoCodeEditor.vue(components/common/code/)→ @guolao/vue-monaco-editor。Monaco 被拆进独立 monaco-vendor chunk,而 SourcePane 本身挂在详情页路由 chunk 上——即"打开详情页只读模式"仍不会下载 Monaco,只有进入编辑态渲染 MonacoCodeEditor 时才拉取(编辑器组件仅编辑态挂载)。Shiki 与之互补:只读高亮走 utils/highlighter.ts 的单例 createHighlighterCore + oniguruma 引擎,14 种语言以内联 mjs 静态注册(随 chunk 下载但不需全量 bundle),2 个主题(github-dark/light)——ADR-5 的"单例 + 内联注册"是 Shiki 体积控制的关键。
ECharts:utils/echarts-setup.ts 的文件头注释记录了一次真实优化——"原先在 main.ts 中全量导入 echarts,导致 ~1.1MB 打入入口 chunk;现在改为由 data-visualization 相关路由在加载页面前先加载本模块"。即 ECharts 从"全站税"降级为"可视化页面税";叠加 echarts-vendor 独立 chunk 与 vue-echarts 全局组件按路由注册,内容浏览主路径完全不触碰图表库。useEChart composable 另做了 300ms 防抖 resize 与实例销毁清理,属于运行期内存卫生。
SFC 运行时:浏览器内编译 Vue SFC 的整条链(sfcRuntime.ts、sfc-modules.ts、sfc-typescript.ts)被 manualChunks 归入 sfc-runtime/sfc-compiler 两个 chunk,@vue/compiler-sfc(数百 KB 级)只在触发运行时功能时下载。
生产构建需 8GB 堆(NODE_OPTIONS='--max-old-space-size=8192')并叠加全量类型检查,是全仓库最慢的命令;build:fast 跳过类型检查作为迭代通道。5000+ chunk 的产出使 dist/ 文件数巨大,部署应使用增量/同步型发布工具而非全量删除重建(部署形态讨论见《06-部署运行》§3)。chunkSizeWarningLimit: 600 与 reportCompressedSize: false(省去每 chunk 的 gzip 计算)都是为构建提速的服务性配置。
hash 化 chunk 在发版后有一个经典窗口期:用户停留在旧页面,其持有的旧 chunk 引用(或 index.html 引用的旧模块路径)在下次动态 import 时 404。src/router/index.ts 的 router.onError 捕获 message 匹配 Loading chunk|Failed to fetch dynamically imported module 的错误后整页 window.location.reload(),chunkReloadTriggered 标志位防止多个 chunk 同时失败触发连环刷新。这把"发版后用户端白屏"降级为"一次自动刷新",是本项目的线上自愈机制(用户感知与排查见《12-FAQ》§2.1)。它只覆盖路由级动态 import 失败——组件内 defineAsyncComponent 的失败不经过 router.onError,这是该机制的已知边界。
meta.keepAliveComponent 缓存组件实例,往返切页不重取数不重渲染——与"数据常驻内存"配合,形成二次进入瞬时呈现的体感。VirtualExampleGrid 让大网格的 DOM 数量与可视区挂钩,是 341 个示例的 css-layout 分组能流畅翻页的直接原因。useListFilter)、resize 防抖(useEChart)、useIntervalUpdate/useAnimationLoop 管理演示动画的定时器生命周期(卸载清理)。useResourceFetch 的 data 用 shallowRef,避免大数组被深层响应式代理的开销——列表数据只替换引用,不做细粒度更新。loadingPromise 失败重试语义按分组细化。Cache-Control: immutable、index.html 配 no-cache——这是零代码改动、纯部署配置的收益(当前文档未约定,部署时容易漏)。SPA fallback 与缓存头一起写进部署模板。build:fast + 分步 CI 的构建提速:把 vue-tsc 从 build 中拆出独立并行 job(CI 已是独立步骤,本地保留原样),并把 manualChunks 的规则改为维护性注释清单(规则数已达 19 组,新人难以判断新依赖应落入哪组——在 vite.config 顶部补一张"新依赖 → chunk 归属"决策表)。requestIdleCallback 预取首屏命中概率最高的分组 chunk,把体感首屏进一步提前。VirtualExampleGrid 与 KeepAlive 的内存审计:KeepAlive 缓存的列表页持有全部分组数据引用属于预期行为,但长会话多页签场景下可用 devtools memory 面板做一次基线审计,确认无意外持留(如事件监听未解绑)。总体评价:这个项目的性能设计在"规模"与"简单"之间找到了一个工程上非常成熟的平衡点——没有引入流式 SSR、没有上微前端、没有自研缓存层,而是把懒加载、拆包、缓存、虚拟滚动四个标准武器用到极致,并用注释把每条配置的事故背景记录下来。对后来者最重要的提醒仍然是那条:"优化冲动"先读 vite.config.ts 注释,尤其是 experimentalMinChunkSize 那条——很多性能优化在 1182 个动态组件的规模下会变成性能灾难。
本篇完。下一篇:11-扩展开发指南.md