WebRTC + Shadow DOM 实战:一个高性能网页小游戏平台的技术复盘

最近几个月,我利用业余时间做了一个网页小游戏平台,叫 OmniGame。从最开始的一个想法,到现在上线了 100 多款游戏、支持 P2P 联机,中间踩了不少坑。这篇文章我想从技术角度复盘一下,整个过程中遇到的问题、我们的解决方案,以及一些对浏览器技术的思考。


一、为什么想做这个东西

事情的起因很简单:某天下午我在工位上犯困,想打开浏览器玩个扫雷醒醒神。

结果我搜了一下"在线小游戏",点进前几个网站,体验让我非常难受:

  • 首先是一堆弹窗广告,关完都要十秒钟
  • 然后首屏加载了 6MB 的 JavaScript,转圈转了半天
  • 好不容易游戏出来了,玩了 30 秒,又弹一个"恭喜你获得新手大礼包"
  • 更气人的是,玩完关了标签页,发现我正在写的文档网站样式都变了——游戏的全局 CSS 污染了整个页面

我当时就在想:Web 技术发展这么多年了,做个网页小游戏怎么还是这么 primitive?

于是我就有了一个想法:能不能自己做一个干净的、没有广告的、打开就玩的小游戏平台?

后来越想越觉得可以做,而且可以做得很不一样:

  1. 零广告——不加任何广告 SDK,首屏体积能压到极小
  2. P2P 联机——用 WebRTC 做直连,不经过服务器,延迟低还省钱
  3. 沙盒隔离——用 Shadow DOM 把每款游戏隔离起来,不污染页面

这个项目从今年年初开始做,到现在差不多半年了。这篇文章就聊聊中间几个核心技术点的实现思路和踩坑经验。


二、首屏体积从 6MB 压到 200KB,我们做了什么

第一个坑是性能。

我最开始用 Next.js 搭官网的时候,跑了一下 Lighthouse,首屏性能得分只有 45 分——主要问题就是 JS 包太大了。我看了一下打包分析,光 react、react-dom、next 这几个基础包就快 200KB 了,再加上其他第三方库,首屏轻轻松松就上了 1MB。

当时我就定了一个目标:首屏加载体积必须压到 200KB 以内。

这个目标听起来很极端,但做起来其实就是一个原则:能不引入的依赖,坚决不引入。

2.1 砍掉所有非必要的第三方库

我先把项目里的第三方依赖全部列了一遍:

  • 动画库:Framer Motion(150KB)→ 换成 CSS transition + Web Animations API
  • 图标库:@fortawesome/fontawesome(200KB)→ 换成 inline SVG
  • 工具库:lodash(70KB)→ 换成自己写的几个工具函数
  • 日期处理:dayjs(7KB)→ 根本用不到,直接删了

砍完之后,整个项目的运行时依赖就剩 react、react-dom、next 这三个基础包。生产环境打包出来的 JS,首屏 chunk 大概 120KB,gzip 之后大概 40KB。

2.2 代码分割做到极致

Next.js 的 App Router 自带代码分割,但很多人用得不好——所有组件都打到首屏 chunk 里了。

我们的做法是:

  • 首页的游戏列表,只加载前 20 个游戏的元数据,其他的全部懒加载
  • 每一款游戏的详情、联机功能,都是独立的 chunk,用户点进去才加载
  • 后台管理页面、设置页面,全部都是动态 import,根本不进首屏

这样做的结果是:用户打开首页,不需要加载"游戏详情页"的代码、不需要加载"联机引擎"的代码、不需要加载"后台管理"的代码——只加载首页本身需要的东西。

2.3 静态资源的优化

图片方面,我们把所有游戏缩略图都转成了 WebP 格式,尺寸控制在 20KB 以内。而且我们不会在首屏加载所有图片——用 IntersectionObserver 做了懒加载,视口内的图片才加载,视口外的不加载。

字体方面,我们直接用了系统默认字体栈,不加载任何网络字体:

font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 
  "Helvetica Neue", Arial, sans-serif;

这样做有两个好处:一是省了字体加载的体积,二是字体渲染更快——系统字体是操作系统自带的,不需要网络下载。

最后加了 Cloudflare CDN,所有静态资源都有严格的缓存策略:JS/CSS 带 content hash,缓存一年;HTML 缓存一分钟。用户第二次访问,几乎就是从本地缓存直接读,打开速度极快。

2.4 效果

做完这些优化之后,我们跑了一下 Lighthouse:

  • 首屏性能得分:98 分
  • 首屏加载体积:gzip 后不到 80KB
  • 首屏加载时间(4G 网络):大概 0.8 秒

对比我最开始点的那些小游戏网站(首屏 6MB+,加载 10 秒+),这个提升是数量级的。


三、WebRTC P2P 联机,我踩过的那些坑

第二个核心技术点是联机。

最开始我想的是用 WebSocket 做中转——实现简单,服务端就是一个 WebSocket 服务器,广播消息就行。但很快我就发现两个问题:

第一,延迟高。你和朋友在同一个办公室,连同一个 Wi-Fi,结果你的操作要先发到几百公里外的云服务器,再发回你朋友的电脑——一个来回 50ms 就没了。对于动作类游戏,这个延迟是不可接受的。

第二,成本高。所有流量都过我们的服务器,用户越多,带宽成本越高。这对于业余项目来说,是个不小的负担。

后来我就想到了 WebRTC——能不能让两台设备直连,不经过我们的服务器?

3.1 WebRTC 的基本原理

简单说一下 WebRTC 是怎么工作的:

  1. 两台设备先通过一个"信令服务器"交换一些信息——SDP 描述、ICE 候选
  2. 然后两台设备尝试打洞——如果 NAT 类型合适,就能直接建立 P2P 连接
  3. 一旦连接建立,数据就直接在两台设备之间传,不经过任何服务器

这个过程听起来简单,但实现起来坑特别多。

3.2 坑一:STUN 服务器的问题

我最开始写了个 demo,两台设备在同一个局域网下,能连上。结果一跨网,就失败了。

查了半天才发现:我根本没配 STUN 服务器。

WebRTC 打洞的原理是:你把自己的本地 IP 和公网 IP(通过 STUN 服务器获取)都告诉对方,然后双方尝试从不同的路径连接。如果不配置 STUN 服务器,你根本不知道自己的公网 IP 是什么,打洞自然就失败了。

后来我加了两个公共的 STUN 服务器:

stun:stun.l.google.com:19302
stun:stun1.l.google.com:19302

跨网连接的成功率一下子就上来了。

3.3 坑二:DataChannel 的配置

最开始我用的是默认的 DataChannel 配置,结果发现:传数据的时候丢包率很高,有时候延迟波动很大。

查了一下文档,发现 DataChannel 有两种模式:

  • unreliable 模式:不保证送达,也不保证顺序,延迟最低
  • reliable 模式:保证送达,保证顺序,延迟高一些

对于游戏来说,我们需要的是:位置更新这种实时性强的数据,用 unreliable 模式;而游戏开始、结束这种关键事件,用 reliable 模式。

后来我针对不同类型的消息,用了不同的 channel 配置,丢包和延迟的问题就解决了。

3.3 坑三:打洞失败怎么办

即使配了 STUN 服务器,还是有一部分用户打洞失败——比如双方都是对称型 NAT,这种情况下直连是不可能的。

这个时候就需要 TURN 服务器做中转。

我自己在服务器上部署了一个 coturn(开源的 TURN 服务器),然后配置到 WebRTC 的 ICE 候选里。如果打洞失败,就自动降级到 TURN 中转。

虽然中转的延迟比直连高,但至少能连上——总比连不上强。

3.4 封装成简单的 API

WebRTC 的底层细节太多了,对于游戏开发者来说太复杂。所以我把所有这些细节都封装起来,对外暴露一个非常简单的 API:

// 创建房间,返回房间号
const roomCode = await OmniNet.createRoom();

// 加入房间
await OmniNet.joinRoom(roomCode);

// 发消息
OmniNet.send({ type: 'move', x: 100, y: 200 });

// 监听消息
OmniNet.onMessage((data) => {
  // 处理对方发来的数据
});

开发者不需要知道什么是 ICE、什么是 SDP、什么是打洞——这些我们全部处理好了。

3.5 实际效果

现在我们的联机效果是:

  • 局域网内延迟:1-5ms,几乎和本地玩一样
  • 跨网直连延迟:30-80ms,取决于双方的网络情况
  • 打洞失败的情况:自动降级到 TURN 中转,延迟 100-200ms,但至少能连上

而且因为流量不经过我们的服务器,带宽成本几乎为零——用户越多,我们越省钱。


四、Shadow DOM 沙盒,为什么不用 iframe

第三个坑是游戏环境的隔离。

最开始我想的很简单:每款游戏放到一个 iframe 里不就行了?iframe 天然就是隔离的。

但实际做的时候,发现 iframe 有几个问题:

第一,性能开销大。每一个 iframe 都是一个独立的浏览器上下文,创建和销毁的开销很大。而且 iframe 之间通信很麻烦——要 postMessage,还要序列化反序列化。

第二,样式不好控制。我想给游戏容器做圆角、做阴影、做过渡动画?很难——因为 iframe 是一个独立的文档,你控制不了它的内部样式。

后来我想到了 Shadow DOM。

4.1 Shadow DOM 是什么

Shadow DOM 是 Web Components 标准的一部分。简单说,就是你可以在普通的 DOM 树里,创建一个"隔离的子树"——这个子树内部的 CSS 和 JS,对外界完全不可见,外界的 CSS 也影响不到它内部。

听起来和 iframe 很像,但它比 iframe 好太多了:

  • 性能好:Shadow DOM 不是独立的浏览器上下文,它就是普通的 DOM,创建销毁几乎没有开销
  • 样式可控:你可以在父页面里给 Shadow DOM 宿主元素做圆角、阴影、动画
  • 通信简单:Shadow DOM 里的 JS 和父页面的 JS 是同一个上下文,直接调用就行

4.2 我们的实现方式

每款游戏的运行方式是这样的:

  1. 先在页面上创建一个普通的 <div> 作为宿主元素
  2. 给这个 div 附加一个 Shadow Root:div.attachShadow({ mode: 'open' })
  3. 把游戏的 HTML、CSS、JS 全部注入到这个 Shadow Root 里面

这样,游戏内部的所有 CSS 都只在 Shadow Root 内部生效,不会泄漏到外面;外面的所有 CSS 也影响不到游戏内部。

JS 方面,我们把游戏的代码放在一个立即执行函数里,避免污染全局变量:

(function() {
  // 游戏代码全部在这里
  // 不会污染 window 全局对象
})();

4.3 效果

现在的效果是:

  • 100 款游戏打开,每一款都在自己的沙盒里,互不干扰
  • 游戏崩了?把对应的 Shadow Root 销毁就行,其他游戏和主页面完全不受影响
  • 游戏的弹窗、按钮、样式,全部都在自己的沙盒里,不会把整个页面搞乱

之前我遇到的那个"游戏 CSS 污染整个页面"的问题,从根本上就解决了。


五、一些对浏览器技术的思考

做这个项目的过程中,我最大的感受是:浏览器已经非常强大了,但很多人还没有把这些能力用好。

WebRTC 出来快十年了,Shadow DOM 也出来很多年了,但真正把它们组合起来做好产品的,并不多。

很多人做网页游戏,第一反应还是用 iframe、用 WebSocket、用 jQuery 写一堆全局脚本——这种老技术栈,体验当然好不了。

但反过来想,这也是机会:因为浏览器的能力一直在变强,但很多产品还停留在十年前的思路。只要你愿意去用这些原生能力,就能做出体验好很多的产品。

5.1 关于"零依赖"的思考

我做这个项目还有一个很深的感受:很多时候,我们引入第三方库,是因为我们觉得这些功能很复杂,但实际上浏览器原生就能做到。

比如动画——以前我们用 jQuery 的 animate,后来用 Framer Motion,现在发现 CSS transition + Web Animations API 就够了,而且性能更好、体积更小。

比如图标——以前我们用图标字体,后来用各种图标库,现在发现 inline SVG 就够了,而且更灵活、体积更小。

比如存储——以前我们用各种状态管理库,现在发现浏览器原生的 localStorage、IndexedDB 就够了。

当然,这不是说第三方库不好——很多时候第三方库确实能提高开发效率。但我们应该先想一下:这个功能,浏览器原生能不能做到?如果能,是不是就不用引入第三方库了?

5.2 关于隐私的思考

做这个项目的时候,我从一开始就没打算加第三方统计 SDK、没打算加广告 SDK。

原因很简单:我觉得用户在浏览器里玩个游戏,没必要把自己的所有行为都交给第三方公司分析。

所以我们的统计就是自己写的极简脚本——只记录页面访问量和游戏点击量,不记录 IP、不记录设备指纹、不追踪用户行为路径。整个统计脚本只有 2KB。

这可能是一个"反潮流"的选择——现在的互联网产品,恨不得把用户的所有行为都记录下来,然后变现。但我总觉得,好的产品应该是"不打扰用户"的。


六、最后

这个项目做了半年,现在算是初具雏形了:100 多款游戏、支持 P2P 联机、首屏秒开、没有广告。

当然还有很多东西没做完:云存档、更多游戏、更完善的联机功能……但至少核心的技术架构已经跑通了。

写这篇文章,一方面是复盘一下这半年踩过的坑,另一方面也是想分享一下:浏览器的原生能力其实已经很强了,只要你愿意去挖掘,就能做出体验很好的产品。

如果你也对 WebRTC、Shadow DOM、或者网页游戏开发感兴趣,欢迎交流。


项目地址:https://www.omnigame.top/

欢迎体验,有问题也欢迎反馈。

posted @ 2026-10-03 09:43  SuperEden  阅读(3)  评论(0)    收藏  举报