Abhishek Singh 打开编辑器时,心里只有一个念头:GoatCounter 的数据很好,但看数据的体验太差了。
这个隐私优先的开源分析工具,没有 cookie、没有弹窗、脚本轻量——但内置仪表盘只有基础表格,没有交互图表,没有世界地图,无法下钻到浏览器版本或地区分布。
![]()
于是他动手做了一个新的。全部代码塞进单个 HTML 文件,React 和图表库从 CDN 加载,浏览器现场编译 JSX。没有构建工具,没有打包流程。
成品在这里:https://abhishekhsingh.github.io/goatcounter-dashboard
GitHub 仓库:https://github.com/abhishekhsingh/goatcounter-dashboard
一、技术选型:为什么故意"开倒车"
现代前端开发默认是 Webpack/Vite + npm install + 本地构建。这位开发者反着来:
React 18 和 Recharts(一个基于 React 的图表库)用 script 标签从 CDN 加载。JSX 交给 Babel standalone 在浏览器里实时编译。样式用原生 CSS 变量做主题切换。五个外部依赖:React、ReactDOM、Recharts、Babel standalone、prop-types(Recharts 运行时需要)。
整个仪表盘约 3000 行代码,全在一个 index.html 里。
听起来像技术债务?他的反驳是:当成一个自包含应用来写,组件边界清晰,维护性并不差。没有构建步骤意味着零配置、零依赖安装、零 CI/CD 管道。复制文件到任意静态托管,立即运行。
这种架构有个隐藏好处: longevity(长期可用性)。npm 生态的依赖地狱是真实存在的,五年后某个子依赖的 break change 可能让你的构建脚本直接挂掉。而 CDN 加载的 React 18,只要浏览器还支持 ES6,页面就能打开。
二、API 限流的攻防战
GoatCounter 的 API 用令牌桶限流:每秒约 4 个请求。但新仪表盘首次加载需要 13 个 API 调用来填充所有卡片。
naive 的做法是 Promise.all 同时发射 13 个请求——结果会被立刻节流,部分请求失败或严重延迟。
他的解决方案是严格顺序队列:每个请求等上一个完全完成(不只是开始),间隔 500 毫秒。算上 CORS 预检请求,实际速率约每秒 2 个。
但 13 个顺序请求 × 500 毫秒 = 6-7 秒加载时间。太慢了。
于是引入懒加载:用 IntersectionObserver(浏览器原生 API,监听元素是否进入视口)分四级触发。首屏可见的卡片立即加载,其余等用户滚动到附近才请求数据。初始加载从 13 个请求砍到 4-5 个,时间控制在 2 秒内。
三、世界地图的体积陷阱
他想要一个 choropleth 地图(国家按访客数着色)。标准方案是运行时加载 d3-geo(D3 的地理投影模块),但 d3 体积太大,从 CDN 拉只为了画张地图,性价比极低。
解法是把计算前置。写了一个一次性的 Node.js 脚本(scripts/build-world-map.js),把 Natural Earth 110m 分辨率的 TopoJSON 数据转换成预投影的 SVG 路径。
这个脚本处理了几何难题:俄罗斯、斐济、阿拉斯加都跨越国际日期变更线,需要 antimeridian wrapping(对穿经线处理)来避免地图绘制错误。投影采用 Natural Earth 投影,输出一个纯 JS 文件,包含 174 个国家的对象——每个对象有 ISO 代码、国家名、预计算的 SVG path d 属性。
生成的 assets/world-map.js 作为独立脚本加载,不走 Babel 编译。这是刻意设计:Babel standalone 每次页面加载都会重新编译整个 type="text/babel" 的代码块,而地图数据有 27KB,混进去会拖慢启动。
最终地图渲染就是简单的 SVG path 拼接,零运行时投影计算,零 d3 依赖。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.